非解除引用的迭代器是否超过了数组未定义行为的“一个接一个”的迭代器?

时间:2016-05-13 12:09:38

标签: c++ arrays pointers iterator language-lawyer

给定int foo[] = {0, 1, 2, 3};我想知道指向过去“一个过去”的迭代器是否无效。例如:auto bar = cend(foo) + 1;

有很多抱怨和警告,这是Stack Overflow问题中的“未定义行为”,如下所示: c++ what's the result of iterator + integer when past-end-iterator? 不幸的是,唯一的来源是挥手。

我在购买时遇到了越来越多的麻烦,例如:

int* bar;

未初始化,但肯定不会调用未定义的行为,并且在给出足够的尝试时,我确信我可以找到一个实例,其中未初始化的bar中的值与cend(foo) + 1具有相同的值。 / p>

这里最大的困惑之一是我询问解除引用cend(foo) + 1我知道这将是未定义的行为,标准禁止它。但是这样的答案:https://stackoverflow.com/a/33675281/2642059只引用解除引用这样的迭代器是非法的回答问题。

我也知道C ++只保证cend(foo)有效,但可能是numeric_limits<int*>::max(),在这种情况下cend(foo) + 1会溢出。我对这种情况不感兴趣,除非它在标准中被调出,因为我们不能让迭代器超过“一个接一个结束”。我知道int*实际上只包含一个整数值,因此会出现溢出。

我想从一个可靠的来源中得到一个引用,即将迭代器移到“一个过去的结束”元素之外是未定义的行为。

4 个答案:

答案 0 :(得分:26)

是的,如果您形成这样的指针,您的程序会有未定义的行为。

那是因为你能做到的唯一方法是将有效指针递增到它指向的对象的边界,这是一个未定义的操作。

  

[C++14: 5.7/5]: 当向指针添加或减去具有整数类型的表达式时,结果具有指针操作数的类型。 如果指针操作数指向数组对象的元素,并且数组足够大,则结果指向与原始元素偏移的元素,以使得结果与原始元素的下标不同数组元素等于整数表达式。换句话说,如果表达式P指向数组对象的 i -th元素,则表达式(P)+N等效,N+(P))和{{1 ((P)-N的值 n )分别指向 i + n -th和 n - n 数组对象的元素,只要它们存在即可。此外,如果表达式N指向数组对象的最后一个元素,则表达式P指向数组对象的最后一个元素之后的一个元素,如果表达式Q指向一个超过最后一个元素的元素一个数组对象,表达式(P)+1指向数组对象的最后一个元素。 如果指针操作数和结果都指向同一个数组对象的元素,或者指向数组对象的最后一个元素,则评估不应产生溢出;否则,行为未定义。

一个未初始化的指针不是同一个东西,因为你从来没有做任何事情&#34;得到&#34;指针,除了声明它(显然是有效的)。但是你甚至无法评估它(不是取消引用 - 评估)而不会给程序带来未定义的行为。直到您为其指定了有效值。

作为旁注,我不会把这些称为“过去的结果”#34;迭代器/指针,C ++中的一个术语,具体指的是&#34; 一个过去的结束&#34;迭代器/指针,它是有效的(例如(Q)-1本身)。你结束了 waaaay 。 ;)

答案 1 :(得分:4)

TL; DR - 计算迭代器超过一个过去的迭代器是未定义的行为,因为在此过程中违反了前提条件。

Lightness提供权威性地涵盖指针的引用。

对于迭代器,通常不会禁止递增超过“end”(一个过去的最后一个元素),但是大多数各种迭代器都禁止它:

Iterator requirements

InputIterator requirements

输入迭代器要求,特别是只有可解除引用的子句,通过引用合并到正向,双向和随机访问迭代器中。

输出迭代器不受限制,它们总是可递增的。因为没有结束,所以过去一次过去的迭代器被排除在外,所以担心它们是否合法计算是没有实际意义的。

然后,在序列中向前跳转是根据个别增量来定义的,因此我们得出结论,对于所有迭代器类型,计算过去的一个过去的迭代器是无意义的或非法的< /强>

RandomAccessIterator requirements

答案 2 :(得分:0)

  

我对这种情况不感兴趣,除非它在标准中被调出,因为我们不能让迭代器超过&#34;一个过去的结束&#34;。我知道int *实际上只是一个整数值,因此可能会溢出。

标准没有讨论未定义的原因。你已经倒退了逻辑:它未定义的事实是一个实现可能会将一个对象放在一个位置,这样做会导致溢出。如果一个&#34;两个过去的结束&#34;迭代器必须是有效的,然后需要实现 not 将对象放在可能导致此类操作溢出的地方。

答案 3 :(得分:0)

正如@ Random842所说:

  

标准没有将指针类型描述为处于具有最小值和最大值的平坦线性空间中,并且它们之间的所有内容都是有效的,因为您似乎认为它们是

指针不被认为存在于扁平线性空间中。相反,有有效的指针和无效的指针。指定了一些指针操作,其他操作是未定义的行为。

在许多现代系统中,指针是在扁平线性空间中实现的。即使在这些系统上,形成一些指针的不确定性也可以打开你的C ++编译器进行一些优化;例如,int foo[5]; bool test(int* it1) { int* it2 = cend(foo); return it1 <= it2; }可以优化为true,因为 no 指针可以与it2进行有效比较且不小于或等于它的指针。

在不太习惯的情况下(比如某些循环),这可以节省每个循环的循环次数。

指针模型不太可能是在考虑到这一点的情况下开发的。在平坦的线性空间中存在的指针实现。

分段内存最为人所知。在旧的x86系统中,每个指针都是一对16位值。他们在线性20位地址空间中引用的位置是high << 4 + lowsegment << 4 + offset

对象位于段内,并具有恒定的段值。这意味着所有已定义的指针<比较可以简单地比较{16}的offset 。他们不必进行数学运算(当时价格昂贵),他们可以丢弃高16位并在订购时比较偏移值。

还有其他架构,其中代码存在于数据的并行地址空间上(因此将代码指针与数据指针进行比较可以返回虚假的相等性。)

规则非常简单。可以创建指向数组中元素的指针,也可以创建指向一端的指针(这意味着分段内存系统无法构建到达段末端的数组)。

现在,你的记忆没有被分割,所以这不是你的问题,对吧?编译器可以自由地解释您在某个代码分支上形成ptr+2以表示ptr 不是指向数组最后一个元素的指针,并相应地进行优化。如果这不是真的,那么您的代码可能会以意想不到的方式运行。

并且有一些实际的编译器在野外使用之类的技术(假设代码不使用未定义的行为,从中证明不变量,使用结论来改变行为 >发生未定义的行为),如果不是那种特殊情况。未定义的行为可以耗费时间,即使底层硬件实现对它没有任何问题&#34;没有任何优化。