【问题标题】:Are end+1 iterators for std::string allowed?是否允许 std::string 的 end+1 迭代器?
【发布时间】:2016-02-12 22:16:03
【问题描述】:

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

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

std::string 与我推测的任何其他标准容器之间的显着区别在于,它始终包含比其 size(零终止符)多一个元素,以满足 .c_str() 的要求。

21.4.7.1 basic_string 访问器[string.accessors]

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

1 返回:一个指针p,使得p + i == &operator[](i) 对应[0,size()] 中的每个i
2 复杂性:恒定时间。
3 要求:程序不得更改字符数组中存储的任何值。

尽管如此,即使它应该恕我直言,保证所述表达式是有效的,为了与零终止字符串的一致性和互操作性,如果没有别的,我发现的唯一一段对此表示怀疑:

21.4.1 basic_string 一般要求[string.require]

4 basic_string 对象中的类字符对象应连续存储。也就是说,对于任何basic_string 对象s,身份&*(s.begin() + n) == &*s.begin() + n 应适用于n 的所有值,这样0 <= n < s.size()

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

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

【问题讨论】:

  • 我很确定如果你增加结束迭代器,结果是未定义的行为。
  • 不要试图打破抽象。不要假设末尾有 0,除非您致电 c_str()
  • 也许问题的要点是std::string 是否有效地允许*end(str)。在实践中,assert(*end(str) == '\0') 应该永远不会失败,但它是正确的 C++11 吗?
  • @ZanLynx:嗯,string_ref 不是string。并且有充分的理由,它没有后者提供的额外保证。
  • @user3164339 lengthsize 相同:en.cppreference.com/w/cpp/string/basic_string/size

标签: c++ string iterator language-lawyer stdstring


【解决方案1】:

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


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

  1. 它试图带来 C 兼容性,其中已知存在一个额外的 \0 字符,超出了 strlen 报告的长度。
  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/ 要求: pos <= size()

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

5/ 抛出: out_of_range if pos >= size()

是的,你没看错,s[s.size()] 返回一个对 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/ 返回: 一个迭代器,它是 past-the-end 值。

非常平淡。

所以我们必须参考其他段落。指针由

提供

§21.4 [basic.string]

3/ basic_string 支持的迭代器是随机访问迭代器 (24.2.7)。

§17.6 [requirements] 似乎没有任何相关内容。因此,字符串迭代器只是普通的旧迭代器(您可能会感觉到这是怎么回事......但是既然我们已经走到了这一步,让我们一路走下去)。

这导致我们:

24.2.1 [iterator.requirements.general]

5/ 正如指向数组的常规指针保证有一个指针值指向数组的最后一个元素,所以对于任何迭代器类型,都有一个迭代器值指向对应序列的最后一个元素。这些值称为过去的值。定义了表达式*i 的迭代器i 的值称为可解引用。该库从不假定 past-the-end 值是可取消引用的。 [...]

所以,*s.end() 格式不正确。

24.2.3 [input.iterators]

2/ 表 107 -- 输入迭代器要求(除了迭代器)

++rr++ 的前置条件列表,r 可以取消引用。

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

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

  • r += n 相当于 [inc|dec]rememting r n
  • a + nn + a 相当于复制a,然后将+= n 应用到副本

-= n- n 也是如此。

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

【讨论】:

  • *s.end() 不是格式错误的:它在语法上是有效的,不违反任何“可诊断的语义规则”并且不违反 ODR。同样,*s.end() + 1 也不是格式错误的。这两个表达式实际上都是格式良好的。
  • 那么它在哪里说结束迭代器不可取消引用?仅仅因为库从不假设它们是可取消引用的,并不能使它如此。
  • @NathanOliver:我想我明白你的意思了;我最初将其读作“库永远不会保证您可以取消引用过去的值”,而我现在看到它可以读作“每当您将[begin, end) 范围提交给库提供的方法/函数时,它永远不会取消引用 end" 这是完全不同的。
  • 那么这个答案是不正确的。您假设 *s.end() 格式不正确,但您在此答案中提出的任何内容实际上都没有说明它是或不是。您必须证明 s.end() 不可取消引用。我有同样的答案并删除它,因为我找不到。
  • @NathanOliver:可能是也可能不是,这取决于 24.2.1/5 的读数。我现在没有时间去重新检查标准以进一步说明应该如何阅读它。
【解决方案2】:

返回:一个指针p,使得p + i == &operator[](i) 对应[0,size()] 中的每个i

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

5.7 声明“对于 [运算符 + 和 -] 而言,指向非数组对象的指针与指向长度为 1 且对象类型作为其元素类型的数组的第一个元素的指针的行为相同。”

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

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

(请注意,这与“格式错误”不同。*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
}

如果不是,为什么不是?

为了与零终止字符串的一致性和互操作性,如果没有别的的话

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

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

应该防止新代码依赖它...为什么? [...] 理论上什么不能让你买账,在实践中又如何?

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

事实上,在我看来,支持end(str) + 1 具有负值,因为尝试使用它的代码本质上会产生与 C 代码相同的问题,C 代码无法确定何时考虑空终止符。对于两种语言,C 都有足够的缓冲区大小错误。

【讨论】:

  • Visual Studio 的例子很引人注目,但它也不是权威的。这种行为是标准纵容的还是 Dirkumware 或 STL 出错了?
  • 没有依赖它的现有代码......可能。至少我一个都不知道。应该防止新代码依赖它......为什么?结论的那部分需要详细说明。 不允许在理论上可以为您带来什么,在实践中又如何?
  • @MatthieuM。我从分析规范到演示实现行为的过渡有点不完整。目的是首先表明它在规范中是未定义的行为(仅仅是因为规范中没有任何地方定义或要求其行为),然后表明实现确实利用了这一点。我已经添加了我认为暗示的缺失短语。
【解决方案3】:

std::basic_string&lt;???&gt; 是其元素的容器。它的元素不包括隐式添加的尾随空值(它可以包括嵌入的空值)。

这很有意义——“对于这个字符串中的每个字符”可能不应该返回结尾的 '\0',因为这实际上是与 C 风格 API 兼容的实现细节。

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

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

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

为了有一个指向过去的迭代器,容器的 const 和非 const 迭代器必须有不同的有效范围,或者是最后一个元素的非 const 迭代器取消引用但未写入的必须存在。

我不寒而栗,让这样的标准措辞无懈可击。

std::basic_string 已经是一团糟了。让它变得更加陌生会导致标准错误,并且会产生不小的成本。收益真的很低;在少数情况下,您希望访问迭代器范围内的所述尾随 ​​null,您可以使用 .data() 并将生成的指针用作迭代器。

【讨论】:

  • DS9K(从 comp.lang.c 导入)如何禁止创建一个超过 NUL 终止符的指针?您必须能够将一个元素视为一个数组,并形成一个刚刚过去的指针,这一规则难道不能保证这一点吗?现在允许非 const 指针指向 const 元素的问题仅用于 std::string 的终止符,这看起来像是可修改迭代器的交易破坏者,并且因为一致性确实是一个好主意,对于常量来说也是如此,是的。
  • @Deduplicator 很容易证明 NUL 终止符与其余数据是连续的。是的,没有办法在 C++ 中实现一个元素,这样一个指针越过它的末尾是无效的;但是我正在想象(但没有提出)的疯狂而复杂的论点是基于 C++ 标准不要求 C++ 标准库在 C++ 中实现的事实。它指定了某些行为。我们保证int x; (&amp;x)+1; 是可以的,但我们对最后一个元素的所有都是一个指针,如*p == '\0' 和一个相同的引用。
  • @Deduplicator 我不认为基于这样一个论点(如果它是有效的)来实现是合理的,我会考虑在一个编译器证明该标准有缺陷,但我是说存在这样的论点,甚至可能是有效的论点。我可能是错的;我对这样一个论点不感兴趣,即使它是正确的,它指出了一个对现实世界没有影响的标准缺陷(因为没有人会利用它,不,真的)。
  • 谢谢,那个让我发笑,因为这个论点太可笑了,但严格来说,根据标准的字母可能真的有效。是的,我同意任何实际采用这种方式的实施者都是没有道理的,或者只是试图搞砸用户的思想......
  • 我相信字符串实现在内部使用非连续分配来保存字符串是合法的,直到有人请求指向后备存储的指针,不是吗?这样的事情可以提高重复连接到字符串的性能,因为每个新字符串的连接只需要为新内容的副本和指向早期字符串的指针分配足够的空间[当它们变得太大时合并链,或者当代码要求一个指向字符串内容的指针时]。在这样的系统中,尾随零可能根本不存储......
【解决方案4】:

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

[string.insert]/15

constexpr iterator insert(const_iterator p, charT c);
前提条件:p*this 上的有效迭代器。

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

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

【讨论】:

    猜你喜欢
    • 2015-04-29
    • 2021-06-09
    • 1970-01-01
    • 1970-01-01
    • 2013-06-24
    • 2020-01-16
    • 2021-10-21
    • 2018-07-24
    • 1970-01-01
    相关资源
    最近更新 更多