为什么 "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++
。这可以与指针混淆,因为只有当 p
和 q
指向相同的位置时,(*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++
的值计算之前排序+
。
以下事情一定会按以下顺序发生:
- 阅读
i
。
++
的值计算(操作数:result-of-step-1)
+
的值计算(操作数步骤 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
中,不会出现该问题。
我知道这个问题在它的 "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++
。这可以与指针混淆,因为只有当 p
和 q
指向相同的位置时,(*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 samei
.
不是,加法运算符的值计算使用其操作数的值计算结果。 +
的操作数是 i++
(不是 i
)和 1
。正如您在问题中所提到的,i
的读取是 sequenced-before i++
的值计算,因此(传递性)在 i++
的值计算之前排序+
。
以下事情一定会按以下顺序发生:
- 阅读
i
。 ++
的值计算(操作数:result-of-step-1)+
的值计算(操作数步骤 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
中,不会出现该问题。