`std::string::begin()`/`std::string::end()` 会使迭代器失效吗?

5
#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() constend()的结果混合在一起。两个函数都不允许使任何迭代器失效。然而,我很好奇end()不能使迭代器变量begin失效是否意味着变量begin可以与end一起使用。
考虑一个C++98下写时复制实现的std::string;非const的begin()end()函数会导致内部缓冲区被复制,因为这些函数的结果可用于修改该字符串。因此,在上述代码中,begin一开始对于ss2都是有效的,但使用非const的end()成员使得它对于产生它的容器s2无效。
上述代码在使用写时复制实现(如libstdc++)时会产生“意外”的结果。而不是end-begins2.size()相同,libstdc++会产生另一个数字
  • 使begin不再成为对s2的有效迭代器,即被检索的容器,是否构成“使迭代器失效”?如果查看迭代器的要求,则在调用.end()后,它们都似乎仍然适用于此迭代器,因此begin仍然可以作为有效迭代器,因此未被使其无效?

  • 上述代码在C++98中是否定义良好?在禁止写时复制实现的C++11中呢?

根据我自己的简短阅读规范,它似乎规定不足,因此可能没有任何保证可以将begin()end()的结果一起使用,即使不混合const和非const版本。


1
C++11明确禁止COW的原因正是这个问题:你的代码是合规的,应该得到“6”的结果,但显然并没有。COW实现是不合规的。 - Lightness Races in Orbit
libc++做得很好。现场演示 - Baum mit Augen
@BaummitAugen 在某种“正确”的定义下。问题中的代码在C++11之前是不合法的,并且它不能与C++11之前的库一起使用(包括g++附带的标准库)。如果库失败了,那并不意味着库是错误的,而是代码本身有问题。 - James Kanze
@JamesKanze “正确”是根据我编译的标准定义的。我的评论并不是一个答案,而是一个评论。 - Baum mit Augen
4个回答

6
正如您所说,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++03中,引用、指针和迭代器引用basic_string序列的元素可能会被以下使用basic_string对象的操作所使无效:调用非const成员函数(除了operator、at()、begin()、rbegin()、end()和rend())。 - Lightness Races in Orbit

2
代码是正确的:在迭代器或元素引用存在危险时,几乎需要进行CoW实现来解除共享。也就是说,当你有一个访问一个字符串中的元素的东西,并且它的副本冒险做同样的事情,即使用迭代器或下标运算符时,它将不得不取消共享。它可以知道它的迭代器并根据需要更新它们。
当然,在并发系统中,要做到所有这些几乎是不可能的,但在C++11之前没有数据竞争。

代码不正确,因为在调用s2.end()时取消共享将使之前对s1.begin()的调用返回的迭代器无效(通过const引用)。 (另外,当然:你在最后一句话中忘了一个或两个单词。正确使用互斥锁,就可以简单地避免任何数据竞争。你无疑是指“在没有数据竞争且具有可接受性能的情况下几乎不可能做到这一点”。) - James Kanze
@JamesKanze:如果在使用s2.end()后取消共享会使事情无效,那么取消共享需要在调用s2.begin()时进行。 - Dietmar Kühl
不是我理解的那样。他对begin()的调用是通过一个const左值,因此他正在调用begin() const。取消共享将使迭代器无效,并且在调用begin() const时,实现不允许使迭代器无效。 - James Kanze
@JamesKanze:迭代器失效的规则并没有真正改变。在我的看法中,CoW实现始终需要跟踪是否已经获取了迭代器或元素的引用。当在第二个字符串上获取第一个迭代器或元素引用时,它需要取消共享:此时没有其他可能会失效的迭代器或引用。 - Dietmar Kühl
在获取第一个非const迭代器或元素引用时,实现需要解除共享。这就是§21.3./5中最后一点的含义。 - James Kanze

2
截至N3337(实质上等同于C++11),规范如下([string.require]/4):
引用、指针和迭代器,指向basic_string序列的元素,可能会被以下使用basic_string对象所使其失效:
[...]
- 调用非const成员函数,除了operator[]、at、front、back、begin、rbegin、end和rend。
至少我理解,这意味着调用beginend不允许使任何迭代器失效。虽然没有直接说明,但我也认为任何对const成员函数的调用都不能使任何迭代器失效。
这个措辞至少保持到n4296。

n4296晚于C++14,所以这不能回答关于C++98和C++11的问题。然而,由于使用了相同(或类似)的措辞,结论在这些标准中是相同的。 - Lightness Races in Orbit
相同的文本存在于C++98和C++11中,但是如果您查看迭代器的要求,它们似乎都在调用end()后保留在我的变量begin上。因此,尽管使用end()的结果无法使用,但似乎begin从技术上来说根本没有失效。只是似乎没有任何要求需要begin()end()的结果可以一起使用。 - bames53
C++11之前的C++要求有很大不同。通过C++98的CD2,对非const []at()begin()end()的任何调用都可能使迭代器、引用和指针失效。在CD2和C++98之间,委员会试图修复这个问题,只说第一次调用可能使迭代器失效。在C++11中,他们改为说没有调用会使其失效,这有效地禁止了写时复制。在所呈现的代码中,程序通过调用begin() const获取迭代器,然后调用end(),这在C++11之前可能会使第一个迭代器失效。 - James Kanze

1
"C++98 [lib.basic.string]/5规定:"
参考、指针和引用basic_string序列元素的迭代器可能会被以下使用basic_string对象的操作使其失效:
  • 作为非成员函数swap(),operator>>()和getline()的参数。
  • 作为basic_string::swap()的参数。
  • 调用data()和c_str()成员函数。
  • 调用非const成员函数,除了operator[](),at(),begin(),rbegin(),end()和rend()。
  • 在上述任何一种使用之后(除具有返回迭代器的insert()和erase()形式之外),第一次调用非const成员函数operator[](),at(),begin(),rbegin(),end()或rend()。

由于s2的构造函数是一个“非常量成员函数”,因此符合最后一个标记所述的第一次对非常量s2.end()的调用会使迭代器失效。因此,根据C++98,该程序没有定义行为。

我不会评论C++11,因为我认为其他答案已经清楚地解释了在该上下文中程序具有定义行为。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接