`std::string::begin()`/`std::string::end()` 导致迭代器失效?
Iterator invalidation by `std::string::begin()`/`std::string::end()`?
#include <string>
#include <iostream>
int main() {
std::string s = "abcdef";
std::string s2 = s;
auto begin = const_cast<std::string const &>(s2).begin();
auto end = s2.end();
std::cout << end - begin << '\n';
}
此代码将 begin() const
的结果与 end()
的结果混合。这些函数都不允许使任何迭代器失效。但是我很好奇 end()
不使迭代器变量 begin
无效的要求是否实际上意味着变量 begin
可用于 end
.
考虑 std::string
的 C++98 写时复制实现;非常量 begin()
和 end()
函数导致复制内部缓冲区,因为这些函数的结果可用于修改字符串。所以上面的 begin
开始对 s
和 s2
都有效,但是使用非常量 end()
成员导致它不再对 s2
有效], 生产它的容器。
以上代码通过写时复制实现产生 'unexpected' 结果,例如 libstdc++。而不是 end - begin
与 s2.size()
相同,libstdc++ produces another number.
导致 begin
不再是 s2
的有效迭代器,从中检索它的容器是否构成 'invalidating' 迭代器?如果您查看迭代器的要求,在调用 .end()
之后,它们似乎都适用于此迭代器,因此也许 begin
仍然有资格作为有效的迭代器,因此尚未失效?
上面的代码在C++98中定义的好吗?在 C++11 中,哪个禁止写时复制实现?
根据我自己对规范的简要阅读,它似乎没有明确规定,因此可能无法保证 begin()
和 end()
的结果可以一起使用,即使不混合 const 和非常量版本。
代码没问题:当迭代器存在危险或持有对元素的引用时,非常需要 CoW 实现来取消共享。也就是说,当您有一些东西访问了一个字符串中的一个元素并且它的副本冒险做同样的事情时,即使用迭代器或下标运算符,它必须被取消共享。它可以知道它的迭代器并根据需要更新它们。
当然,在并发系统中,如果没有数据竞争,几乎不可能做到所有这些,但是在 C++11 之前,没有数据竞争。
自 N3337 (which is essentially identical to C++11) 起,规格为 ([string.require]/4):
References, pointers, and iterators referring to the elements of a basic_string sequence may be invalidated by the following uses of that basic_string object:
[...]
- Calling non-const member functions, except operator[], at, front, back, begin, rbegin, end, and rend.
至少正如我读到的那样,这意味着不允许调用 begin
或 end
使任何迭代器无效。虽然没有直接说明,但我也认为这意味着没有调用 const
成员函数可以使任何迭代器无效。
这个措辞至少在 n4296 之前保持不变。
正如您所说,C++11 在这方面与早期版本不同。在 C++11 中没有问题,因为所有允许写时复制的尝试都被删除了。在 C++11 之前的版本中,您的代码会导致未定义的行为;允许调用 s2.end()
使现有的迭代器无效(在 g++ 中确实如此,也许仍然如此)。
请注意,即使 s2
不是副本,标准也会允许它使迭代器无效。事实上,C++98 的 CD 甚至做出了 f( s.begin(), s.end() )
或 s[i] == s[j]
等未定义行为。这只是在最后一刻才意识到,并已更正,以便只有第一次调用 begin()
、end()
或 []
才能使迭代器无效。
C++98 [lib.basic.string]/5 状态:
References, pointers, and iterators referring to the elements of a basic_string
sequence may be invalidated by the following uses of the basic_string
object:
As an argument to non-member functions swap()
, operator>>()
, and getline()
.
As an argument to basic_string::swap()
.
Calling data()
and c_str()
member functions.
Calling non-const member functions, except operator[]()
, at()
, begin()
, rbegin()
, end()
, and rend()
.
Subsequent to any of the above uses except the forms of insert()
and erase()
which return iterators, the first call to non-const member functions operator[]()
, at()
, begin()
, rbegin()
, end()
, or rend()
.
由于 s2
的构造函数是一个“非常量成员函数”,因此它符合对非常量 s2.end()
的调用 - 根据上面最后一个项目符号的第一个这样的调用 -使迭代器无效。因此,该程序没有按照 C++98 定义的行为。
我不会评论 C++11,因为我认为其他答案清楚地解释了该程序在该上下文中定义了行为。
#include <string>
#include <iostream>
int main() {
std::string s = "abcdef";
std::string s2 = s;
auto begin = const_cast<std::string const &>(s2).begin();
auto end = s2.end();
std::cout << end - begin << '\n';
}
此代码将 begin() const
的结果与 end()
的结果混合。这些函数都不允许使任何迭代器失效。但是我很好奇 end()
不使迭代器变量 begin
无效的要求是否实际上意味着变量 begin
可用于 end
.
考虑 std::string
的 C++98 写时复制实现;非常量 begin()
和 end()
函数导致复制内部缓冲区,因为这些函数的结果可用于修改字符串。所以上面的 begin
开始对 s
和 s2
都有效,但是使用非常量 end()
成员导致它不再对 s2
有效], 生产它的容器。
以上代码通过写时复制实现产生 'unexpected' 结果,例如 libstdc++。而不是 end - begin
与 s2.size()
相同,libstdc++ produces another number.
导致
begin
不再是s2
的有效迭代器,从中检索它的容器是否构成 'invalidating' 迭代器?如果您查看迭代器的要求,在调用.end()
之后,它们似乎都适用于此迭代器,因此也许begin
仍然有资格作为有效的迭代器,因此尚未失效?上面的代码在C++98中定义的好吗?在 C++11 中,哪个禁止写时复制实现?
根据我自己对规范的简要阅读,它似乎没有明确规定,因此可能无法保证 begin()
和 end()
的结果可以一起使用,即使不混合 const 和非常量版本。
代码没问题:当迭代器存在危险或持有对元素的引用时,非常需要 CoW 实现来取消共享。也就是说,当您有一些东西访问了一个字符串中的一个元素并且它的副本冒险做同样的事情时,即使用迭代器或下标运算符,它必须被取消共享。它可以知道它的迭代器并根据需要更新它们。
当然,在并发系统中,如果没有数据竞争,几乎不可能做到所有这些,但是在 C++11 之前,没有数据竞争。
自 N3337 (which is essentially identical to C++11) 起,规格为 ([string.require]/4):
References, pointers, and iterators referring to the elements of a basic_string sequence may be invalidated by the following uses of that basic_string object:
[...]
- Calling non-const member functions, except operator[], at, front, back, begin, rbegin, end, and rend.
至少正如我读到的那样,这意味着不允许调用 begin
或 end
使任何迭代器无效。虽然没有直接说明,但我也认为这意味着没有调用 const
成员函数可以使任何迭代器无效。
这个措辞至少在 n4296 之前保持不变。
正如您所说,C++11 在这方面与早期版本不同。在 C++11 中没有问题,因为所有允许写时复制的尝试都被删除了。在 C++11 之前的版本中,您的代码会导致未定义的行为;允许调用 s2.end()
使现有的迭代器无效(在 g++ 中确实如此,也许仍然如此)。
请注意,即使 s2
不是副本,标准也会允许它使迭代器无效。事实上,C++98 的 CD 甚至做出了 f( s.begin(), s.end() )
或 s[i] == s[j]
等未定义行为。这只是在最后一刻才意识到,并已更正,以便只有第一次调用 begin()
、end()
或 []
才能使迭代器无效。
C++98 [lib.basic.string]/5 状态:
References, pointers, and iterators referring to the elements of a
basic_string
sequence may be invalidated by the following uses of thebasic_string
object:
As an argument to non-member functions
swap()
,operator>>()
, andgetline()
.As an argument to
basic_string::swap()
.Calling
data()
andc_str()
member functions.Calling non-const member functions, except
operator[]()
,at()
,begin()
,rbegin()
,end()
, andrend()
.Subsequent to any of the above uses except the forms of
insert()
anderase()
which return iterators, the first call to non-const member functionsoperator[]()
,at()
,begin()
,rbegin()
,end()
, orrend()
.
由于 s2
的构造函数是一个“非常量成员函数”,因此它符合对非常量 s2.end()
的调用 - 根据上面最后一个项目符号的第一个这样的调用 -使迭代器无效。因此,该程序没有按照 C++98 定义的行为。
我不会评论 C++11,因为我认为其他答案清楚地解释了该程序在该上下文中定义了行为。