【问题标题】:What are the differences between a+i and &a[i] for pointer arithmetic in C++?C++ 中指针运算的 a+i 和 &a[i] 有什么区别?
【发布时间】:2019-03-01 05:45:16
【问题描述】:

假设我们有:

char* a;
int   i;

许多对 C++ 的介绍(如 this one)表明右值 a+i&a[i] 可以互换。几十年来我天真地相信了这一点,直到最近我偶然发现了以下引用自 [dcl.ref] 的文字 (here):

特别是,空引用不能存在于定义良好的程序中,因为创建此类引用的唯一方法是将其绑定到通过取消引用空指针获得的“对象”,这会导致未定义的行为。

换句话说,将引用对象“绑定”到 null 取消引用会导致未定义的行为。基于context of the above text,可以推断出仅评估 &a[i](在offsetof 宏内)被认为是“绑定”引用。此外,似乎有一个共识,即&a[i]a=nulli=0 的情况下会导致未定义的行为。这种行为不同于a+i(至少in C++, in the a=null, i=0 case)。

这导致了至少 2 个关于 a+i&a[i] 之间差异的问题:

首先,a+i&a[i] 之间的潜在语义差异是什么导致了这种行为差异。是否可以用任何一种一般原则来解释,而不仅仅是“将引用绑定到空解引用对象会导致未定义的行为仅仅因为这是每个人都知道的非常具体的情况”?是不是&a[i] 可能会生成对a[i] 的内存访问?或者规范作者那天对 null 取消引用不满意?还是别的什么?

其次,除了a=nulli=0 的情况之外,还有其他情况a+i&a[i] 的行为不同吗? (可能包含在第一个问题中,具体取决于它的答案。)

【问题讨论】:

  • 根据答案here,如果a=nulla+i 是未定义的,尽管你的第四个链接说它定义如果i=0,嗯
  • @kmdreko。那是个很好的观点。我已经调整了差异描述以关注a=nulli=0 的情况,以确定a+i&a[i] 之间存在a 差异......再次,导致人们想知道如果它们之间有任何其他差异。
  • a 是空指针时,标准的意图绝不是禁止&*a。这是issue 232的主题。
  • @n.m.非常有趣的是,提议的解决方案只指定了在anull 或“一个数组的最后一个元素之后”的情况下会发生什么。这些几乎是空左值的两个最有用的例子!但是为什么他们停在那里而不只是让a+i&a[i] 完全等效...?
  • 更可怕的是,a[i]i[a] 实际上是可以互换的……因为 C.(试试看。)

标签: c++ language-lawyer pointer-arithmetic


【解决方案1】:

TL;DR:根据标准(意图),a+i&a[i] 都是格式正确的,并且当 a 是空指针且 i 为 0 时会生成空指针,并且所有编译器都同意。


a+i 显然符合最新标准草案的[expr.add]/4

当一个整数类型的表达式 J 被添加到一个指针类型的表达式 P 中或从一个表达式 P 中减去时,结果具有 P 的类型。

  • 如果 P 的计算结果为空指针值,而 J 的计算结果为 0,则结​​果为空指针值。
  • [...]

&a[i] 很棘手。根据[expr.sub]/1a[i] 等价于*(a+i),因此&a[i] 等价于&*(a+i)。现在标准还不太清楚当a+i 是空指针时&*(a+i) 是否格式正确。但作为@n.m。在comment 中指出,cwg 232 中记录的意图是允许这种情况。


由于核心语言UB需要被一个常量表达式([expr.const]/(4.6))捕获,我们可以测试编译器是否认为这两个表达式是UB。

这里是演示,如果编译器认为static_assert 中的常量表达式是UB,或者如果他们认为结果不是true,那么他们必须根据标准生成诊断(错误或警告):

(请注意,这使用了单参数 static_assert 和 constexpr lambda,它们是 C++17 的特性,而默认的 lambda 参数也是相当新的)

static_assert(nullptr == [](char* a=nullptr, int i=0) {
    return a+i;
}());

static_assert(nullptr == [](char* a=nullptr, int i=0) {
    return &a[i];
}());

https://godbolt.org/z/hhsV4I 看来,在这种情况下,所有编译器的行为似乎都是一致的,根本不会产生任何诊断(这让我有点惊讶)。


但是,这与offset 的情况不同。 that question 中发布的实现显式创建了一个引用(这是回避用户定义的operator& 所必需的),因此受引用要求的约束。

【讨论】:

  • 由于核心语言 UB 需要被捕获在一个常量表达式中 [expr.const] 说 "... 将具有未定义的行为,如 .. ." 我一直认为必须明确指定 UB。当p == nullptr 时,没有明确的措辞表明*p 是UB。
  • @LanguageLawyer 好吧,如果您的意思是它的格式正确,那么我同意。如果您的意思是“没有明确指定为UB,但仍然是UB”,那么我想您需要证明这种东西的存在。
  • 证明标准没有明确标记为UB的UB的存在?它来自the definition of UB:本国际标准没有要求的行为。如果标准对某些东西没有要求,这就是 UB,即使标准没有明确说明这一点。定义后的注释说:当本国际标准省略任何明确的行为定义时,可能会出现未定义的行为
  • @Language Lawyer 但是expr.unary.op/1 似乎定义了一个基本要求,如果你有一个指针p,那么*p 指定它的内存位置。并不是说它必须是一个有效的位置。所以按照你的逻辑,*pnot UB ...我认为@cpplearner 的逻辑在这里是正确的;正是规范的其他部分,如 [dcl.ref],专门在这里创造了 UB 的可能性。
  • @personal_cloud 我在间接运算符的定义中没有找到任何关于内存位置的信息。它表示生成的左值是指指针表达式指向的对象或函数。而且由于p == nullptr 不指向任何对象或函数,并且这种情况没有被标准明确处理,它被认为是未定义的行为。 AFAIU 因为这个 UB 是隐式的,编译器不需要在常量表达式中诊断它。但我在这里不是 100% 确定。
【解决方案2】:

在 C++ 标准中,[expr.sub]/1 部分可以阅读:

表达式E1[E2]*((E1)+(E2)) 相同(根据定义)。

这意味着&a[i]&*(a+i) 完全相同。因此,您将首先取消引用 * 指针,然后获取地址 &。如果指针无效(即nullptr,但也超出范围),则为 UB。

a+i 基于指针算法。起初它看起来不那么危险,因为没有取消引用肯定是 UB。不过也有可能是UB(见[expr.add]/4

当具有整数类型的表达式被添加或减去时 从一个指针,结果具有指针操作数的类型。如果 表达式 P 指向具有 n 的数组对象 x 的元素 x[i] 元素,表达式 P + J 和 J + P(其中 J 的值为 j) 如果 0 ≤ i + j ≤ 则指向(可能是假设的)元素 x[i + j] n; 否则,行为未定义。同样,表达式 P - J 指向(可能是假设的)元素 x[i - j] 如果 0 ≤ i - j ≤ n;否则,行为未定义。

因此,虽然这两个表达式背后的语义略有不同,但我会说最终的结果是相同的。

【讨论】:

  • 但请参阅 C++17 DIS 的 [expr.add]/7,或当前标准草案的 [expr.add]/(4.1)
  • 感谢您将其分解为&*(a+i);这很有帮助。澄清一下:我认为您是在说,如果我们相信 [expr.add]/4,那么 &* 不会引入任何 (a+i) 尚未创建的 UB 案例? (如果我们不相信 [expr.add]/4,那么 &* 可能会创建 (a+i) 中不存在的 UB 案例)?我想我可以接受这是一个完整的答案。谢谢。
  • @personal_cloud 是的!指针算术和索引确实以非常一致的方式定义,因此它们导致相同的结果(您在问题中已经提到的异常的一部分)。
猜你喜欢
  • 2016-10-11
  • 1970-01-01
  • 1970-01-01
  • 2019-02-20
  • 2013-03-21
  • 1970-01-01
  • 1970-01-01
  • 2015-08-26
  • 2013-05-06
相关资源
最近更新 更多