是否允许 std::string 的 end+1 迭代器?

Are end+1 iterators for std::string allowed?

std::string 创建指向 end(str)+1 的迭代器是否有效?
如果不是,为什么不是?

这个问题仅限于 C++11 及更高版本,因为在 C++11 之前的版本中,数据已经存储在一个连续的块中,但很少有 POC 玩具实现,数据没有 以这种方式存储。
我认为这可能会有所不同。

std::string 和我推测的任何其他标准容器之间的显着区别是它总是比它的 size 多包含一个元素,即零终止符,以满足 [= 的要求16=].

21.4.7.1 basic_string accessors [string.accessors]

const charT* c_str() const noexcept;
const charT* data() const noexcept;

1 Returns: A pointer p such that p + i == &operator[](i) for each i in [0,size()].
2 Complexity: Constant time.
3 Requires: The program shall not alter any of the values stored in the character array.

不过,即使 应该 恕我直言,保证所述表达式有效,为了与零终止字符串的一致性和互操作性,如果没有别的,我发现的唯一段落对那:

21.4.1 basic_string general requirements [string.require]

4 The char-like objects in a basic_string object shall be stored contiguously. That is, for any basic_string object s, the identity &*(s.begin() + n) == &*s.begin() + n shall hold for all values of n such that 0 <= n < s.size().

(所有引用均来自 C++14 最终草案 (n3936)。)

相关:Legal to overwrite std::string's null terminator?

Returns: A pointer p such that p + i == &operator[](i) for each i in [0,size()].

std::string::operator[](size_type i) 指定为 return “对类型 charT 的对象的引用,当 i == size() 时值为 charT(),因此我们知道指针指向一个对象。

5.7 表示 "For the purposes of [operators + and -], a pointer to a nonarray object behaves the same as a pointer to the first element of an array of length one with the type of the object as its element type."

所以我们有一个非数组对象,并且规范保证指向它的指针是可表示的。所以我们知道 std::addressof(*end(str)) + 1 必须是可表示的。

然而,这并不是 std::string::iterator 的保证,并且规范中的任何地方都没有这样的保证,这使其成为未定义的行为。

(请注意,这与 'ill-formed' 不同。*end(str) + 1 实际上是合式的。)

迭代器可以并且确实实现了检查逻辑,当您执行诸如递增 end() 迭代器之类的操作时会产生各种错误。这实际上是 Visual Studios 调试迭代器对 end(str) + 1.

所做的
#define _ITERATOR_DEBUG_LEVEL 2
#include <string>
#include <iterator>

int main() {
  std::string s = "ssssssss";
  auto x = std::end(s) + 1; // produces debug dialog, aborts program if skipped
}

And if it isn't, why isn't it?

for consistency and interoperability with zero-terminated strings if nothing else

C++ 指定了一些特定的东西来与 C 兼容,但这种向后兼容性仅限于支持实际上可以用 C 编写的东西。C++ 不一定尝试采用 C 的语义并使新构造的行为类似于某些东西方法。 std::vector 是否应该衰减到迭代器只是为了与 C 的数组衰减行为一致?

我会说 end(std) + 1 被保留为未定义的行为,因为尝试以这种方式约束 std::string 迭代器没有任何价值。没有执行此操作的遗留 C 代码,C++ 需要与之兼容,并且应该阻止新代码执行此操作。

New code should be prevented from relying on it... why? [...] What does not allowing it buy you in theory, and how does that look in practice?

不允许它意味着实现不必支持增加的复杂性,提供零证明价值的复杂性。

事实上,在我看来,支持 end(str) + 1 具有负值,因为尝试使用它的代码本质上会产生与 C 代码相同的问题,C 代码无法弄清楚何时考虑 null终结者与否。对于两种语言,C 都有一个缓冲区大小错误。

A std::basic_string<???> 是其元素之上的容器。它的元素不包括隐式添加的尾随空值(它可以包括嵌入的空值)。

这很有意义 -- "for each character in this string" 可能不应该 return 尾随 '[=11=]',因为这实际上是与 C 风格 API 兼容的实现细节。

容器的迭代器规则基于不在末尾插入额外元素的容器。在没有动机的情况下为 std::basic_string<???> 修改它们是值得怀疑的;只有在有回报的情况下,才应该打破一种工作模式。

有充分的理由认为指向 .data().data() + .size() + 1 的指针是允许的(我可以想象对标准的扭曲解释将不允许它)。所以如果你真的需要 read-only 迭代器到 std::string 的内容中,你可以使用指向常量元素的指针(毕竟,这是一种迭代器).

如果你想要可编辑的,那么不,没有办法得到一个有效的迭代器到最后一个。您也不能合法地获得对尾随 null 的非 const 引用。事实上,这样的访问显然不是一个好主意;如果更改该元素的值,则会破坏 std::basic_string 不变的空终止符。

要有一个指向最后一个的迭代器,指向容器的 const 和非常量迭代器必须具有不同的有效范围,或者指向最后一个元素的非常量迭代器可以取消引用但不能写入必须存在。

让这样标准的措辞滴水不漏,我不寒而栗。

std::basic_string已经乱七八糟了。让它变得更奇怪会导致标准错误,并且会产生不小的成本。收益真的很低;在少数情况下,您想要访问迭代器范围内的尾随空值,您可以使用 .data() 并将结果指针用作迭代器。

TL;DR: s.end() + 1 是未定义的行为。


std::string是个怪兽,主要是历史原因:

  1. 它试图带来 C 兼容性,已知在 strlen 报告的长度之外存在一个额外的 [=12=] 字符。
  2. 它设计有基于索引的界面。
  3. 作为事后的想法,当在标准库中与其余的 STL 代码合并时,添加了一个基于迭代器的接口。

这导致 std::string,在 C++03 中,编号为 103 member functions,此后添加了一些。

因此,应该预料到不同方法之间的差异。


已在基于索引的界面中出现差异:

§21.4.5 [string.access]

const_reference operator[](size_type pos) const;
reference operator[](size_type pos);

1/ Requires: pos <= size()

const_reference at(size_type pos) const; reference at(size_type pos);

5/ Throws: out_of_range if pos >= size()

是的,你没看错,s[s.size()] returns 引用 NUL 字符,而 s.at(s.size()) 抛出 out_of_range 异常。如果有人告诉您将 operator[] 的所有用途替换为 at 因为它们更安全,请当心 string 陷阱...


那么,迭代器呢?

§21.4.3 [string.iterators]

iterator end() noexcept;
const_iterator end() const noexcept;
const_iterator cend() const noexcept;

2/ Returns: An iterator which is the past-the-end value.

非常平淡。

所以我们不得不参考其他段落。

提供了一个指针

§21.4 [basic.string]

3/ The iterators supported by basic_string are random access iterators (24.2.7).

§17.6 [要求] 似乎没有任何相关内容。因此,字符串迭代器只是普通的旧迭代器(您可能会感觉到这是怎么回事……但是既然我们已经走到这一步了,那就继续吧)。

这导致我们:

24.2.1 [iterator.requirements.general]

5/ Just as a regular pointer to an array guarantees that there is a pointer value pointing past the last element of the array, so for any iterator type there is an iterator value that points past the last element of a corresponding sequence. These values are called past-the-end values. Values of an iterator i for which the expression *i is defined are called dereferenceable. The library never assumes that past-the-end values are dereferenceable. [...]

因此,*s.end() 格式错误。

24.2.3 [input.iterators]

2/ Table 107 -- Input iterator requirements (in addition to Iterator)

列出 ++rr++ 的前提条件,即 r 可取消引用。

Forward 迭代器、Bidirectional 迭代器和 Random 迭代器都没有取消此限制(并且都表明它们继承了其前身的限制)。

另外,为了完整性,在24.2.7 [random.access.iterators], Table 111 -- 随机访问迭代器要求(除了双向迭代器) 列出以下操作语义:

  • r += n 相当于 [inc|dec] 记忆 r n
  • a + nn + a等同于复制a,然后将+= n应用到复制

-= n- n 也类似。

因此s.end() + 1是未定义的行为。

我找不到明确的答案,但间接证据表明 end()+1 未定义。

[string.insert]/15

constexpr iterator insert(const_iterator p, charT c);
Preconditions: p is a valid iterator on *this.

期望它与 end()+1 作为迭代器一起工作是不合理的,它确实会导致 libstdc++ 和 libc++ 崩溃。

这意味着 end()+1 不是有效的迭代器,意味着 end() 不可递增。