为什么 "i++ + 1" 本身不是未定义的?是什么确保后缀的副作用发生在 + 的计算之后?

Why is "i++ + 1" itself not undefined? What ensures that the postfix's side-effect occurs after the computation of +?

我知道这个问题在它的 "i = i++ +1" 版本中经常被问到,其中 i 出现了两次,但我的问题不同之处在于,它专门针对此表达式的右侧,其定义对我来说并不明显。我仅指:

i++ + 1;

cppreference.com 声明 here 即:

2) The value computations (but not the side-effects) of the operands to any operator are sequenced before the value computation of the result of the operator (but not its side-effects).

我理解这意味着价值计算是有序的,但没有关于副作用的声明。

[...]

4) The value computation of the built-in post-increment and post-decrement operators is sequenced before its side-effect.

但是,它没有指定(在这种情况下)左操作数的副作用相对于表达式的值计算进行排序。

它进一步指出:

If a side effect on a scalar object is unsequenced relative to a value computation using the value of the same scalar object, the behavior is undefined.

这里不是这样吗? post-inc-operator 对 i 的副作用相对于使用相同 i.

的加法运算符的值计算是无序的

为什么这个表达式通常不被称为未定义?

是否因为加法运算符被认为会引发函数调用,并为其提供更严格的顺序保证?

i++ + 1 不是由于使用后缀运算符而未定义,因为它只对一个对象产生一种副作用,并且该对象的值仅在该位置被引用。 i++ 表达式明确地产生了 i 的先验值,并且该值是与 1 相加的值,无论 i 何时实际更新。

(我们不知道 i++ + 1 是明确定义的,因为其他各种原因可能会出错:i 未初始化或不确定或无效,或者数字溢出或指针超限正在实施。)

如果在同一评估阶段我们尝试修改同一对象两次,则会出现未定义的行为:i++ + i++。这可以与指针混淆,因为只有当 pq 指向相同的位置时,(*p)++ + (*q)++ 才会递增相同的对象;否则没关系。

如果在同一求值阶段,我们尝试观察在表达式其他地方修改的对象的值,如 i++ + i,也会出现未定义的行为。 + 的右侧访问 i,但就左侧 i++ 的副作用而言,它没有排序; + 运算符不强加序列点。在 i++ + 1 中,1 不会尝试访问 i,不用说。

What ensures that the postfix's side-effect occurs after the computation of +?

没有这样的保证。后缀的副作用可能发生在 + .

的值计算之前或之后

The post-inc-operator's side effect on i is unsequenced relative to the value computation of the addition operator, which uses the same i.

不是,加法运算符的值计算使用其操作数的值计算结果。 + 的操作数是 i++(不是 i)和 1。正如您在问题中所提到的,i 的读取是 sequenced-before i++ 的值计算,因此(传递性)在 i++ 的值计算之前排序+

以下事情一定会按以下顺序发生:

  1. 阅读i
  2. ++ 的值计算(操作数:result-of-step-1)
  3. + 的值计算(操作数步骤 2 和 1 的结果)

并且 i++ 的副作用必须发生在步骤 1 之后,但它可以发生在该约束之前的任何地方。

"What ensures that the postfix's side-effect occurs after the computation of +?"

没有任何具体保证。你必须表现得好像你在使用 i 的原始值,并且在某些时候它需要执行副作用,但只要一切正常,编译器如何实现它并不重要或者按什么顺序。它可以(并且在某些情况下,会)将其实现为大致等同于:

auto tmp = i;
i = tmp + 1; // Could be done here, or after the next expression, doesn't matter since i isn't read again 
tmp + 1;  // produces actual value of i++ + 1

auto tmp = i + 1;
i = tmp; // Could be done here, or after the next expression, doesn't matter since tmp isn't changed again
(tmp - 1) + 1; // produces actual value of i++ + 1

或(对于具有足够信息的原语或内联运算符重载)将表达式优化为:

++i; // Usually the same as i++ + 1 if compiler has enough knowledge

因为后缀递增后加一可以视为前缀递增而不加一。

要点是,由编译器来确保有时会发生副作用,这可能发生在+的计算之前或之后;编译器只需要确保它已经存储或可以恢复 i.

的原始值

这里的各种扭曲可能看起来毫无意义(显然 ++i 是最好的,如果你能摆动它,而 i + 1; 然后是 ++i 是最简单的),但它们是通常需要在给定架构上使用硬件原子;如果架构提供 fetch_then_add 指令,您希望将其实现为:

 auto tmp = fetch_then_add(i, 1); // Returns original value of i, while atomically adding 1
 tmp + 1;

但如果它只提供 add_then_fetch 指令,你会想要:

auto tmp = add_then_fetch(i, 1); // Returns incremented value of i
(tmp - 1) + 1;

与许多事情一样,C++ 标准没有强加优先顺序,因为真实的硬件并不总是合作;如果它完成了工作并且按照记录的方式运行,那么它使用什么顺序并不重要。

计算 i++ + 1 时会发生以下情况:

  • 计算子表达式 i++。它产生 i.
  • 的先前值
  • 计算 i++ 也有增加 i 的存储值的副作用——但请注意,不会使用增加的值。
  • 计算子表达式 1,产生明显的值。
  • 计算 + 运算符,产生 i++ 的结果加上 1 的结果。这只有在确定左右子表达式的值后才会发生(但它可以始终发生在副作用发生之前或之后)。

++ 运算符的副作用只能保证在下一个 序列点 之前的某个时间发生。 (这是 C99 的术语。C11 标准以不同的方式呈现相同的规则。)但是由于表达式中的任何其他内容都不依赖于该副作用,因此它何时发生并不重要。没有冲突,所以没有未定义的行为。

i++ + i中,在RHS上对i的评估会根据副作用是否已经发生而产生不同的结果。由于排序未定义,标准举手表示行为未定义。但在 i++ + i 中,不会出现该问题。