“(i+=10)+=10”在哪些版本的C++标准中具有未定义行为?

32
在C++中,以下代码是否具有未定义行为:
int i = 0;
(i+=10)+=10;

我的回答中对C和C ++中+=的结果是什么?有一些争议。这里的微妙之处在于默认响应似乎是“是”,而正确答案似乎是“取决于C ++标准的版本”。
如果它确实取决于标准的版本,请解释在哪里是未定义行为,在哪里不是。

3
是当前的C++还是旧版本? - user784668
8
我理解你认为答案是“是”。在https://dev59.com/aWgv5IYBdhLWcg3wXPou#10653994的评论中有一个相当令人信服的论点,表明答案实际上是“不”。 - NPE
2
@Fanael:两者都可以。回答越完整越好。 - NPE
4
给他规格书,然后我们可以开始讨论如何进行投票。 - user784668
9
可以说,这是关于序列点常见问题的重复,而该问题本身应该根据C++11中的新规则更新序列点。但我认为现在还不足以做出这个论断,最好将现有的FAQ问题明确标注为C++03,并重新开始适用于C++11的提问。 - Steve Jessop
显示剩余9条评论
3个回答

34

tl;dr: 在C++98和C++11中,(i+=10)+=10的修改和读取顺序都被明确定义。然而在C++98中,即使修改顺序被明确定义,如果同一对象没有一个中间的sequence point,多次对其进行修改也会导致未定义行为。而C++11已经废除了sequence point的概念,只要保证对象的修改顺序相对于读取顺序是有序的,就可以得到定义良好的行为。

C++98规定,在没有中间sequence point的情况下,对同一对象进行多次修改会导致未定义行为,即使修改顺序被明确定义。而C++11已经废除了sequence point的概念,只需要保证对象的修改顺序相对于读取顺序是有序的,就可以得到定义良好的行为。

C++11 [intro.execution] 1.9 p15

除非另有说明,否则单个操作符的操作数以及单个表达式的子表达式的计算是无序的。[...]

如果对标量对象的副作用与同一标量对象的另一个副作用或使用相同标量对象值进行的值计算之间的顺序关系不确定,则行为未定义。

C++11 [expr.ass] 5.17 p1

在所有情况下,赋值在右和左操作数的值计算之后顺序执行,并在赋值表达式的值计算之前顺序执行。

因此,尽管有序并不足以使行为在C ++98中被定义,但C ++11已更改要求,即有序(即,按顺序)就足够了。

(而且我认为“序列之前”和“序列之后”提供的额外灵活性已经导致了一种更加明确,一致和规范的语言。)


我认为,即使顺序操作的序列不足以产生技术上定义良好的行为,任何C++98实现也不太可能在其序列化时做出令人惊讶的事情。例如,Clang在C ++98模式下生成的此表达式的内部表示具有定义良好的行为并执行所期望的操作。


然而在C++98中,分配给j的值是不确定的,但行为并非未定义-这是不正确的。C++98规定,在没有干预序列点的情况下,只能读取“i”的先前值以确定要存储的修改值。但是在“(i+=1) + i”中,无法确定“+ i”是读取“i”的先前值还是后续值,因此结果是未定义的行为。 - Johannes Schaub - litb
@JohannesSchaub-litb,您能否给我一个引用,这样我就可以在C++98标准中看到它的上下文了吗? - bames53
@bames53:“C++98语言肯定需要整理”--是的,而这个整理被称为C++11。 - Keith Thompson
"(i+=10)" 的“值计算”是否包括修改“i”的副作用?换句话说,第二个“+=”的副作用是否必须在第一个“+=”的副作用之后序列化?按照C++98的术语,副作用可以在下一个序列点之前的任何时间发生。当然,实现可以计算“i+=10”的结果(即“i+1”)而不实际修改“i”;这在抽象机器中是允许的吗? - Keith Thompson
在C++11中,即赋值给i的副作用与赋值表达式i+=10的值计算是分开且顺序的。赋值在左侧或右侧的值计算之后进行。参见5.17/1。 - bames53
显示剩余7条评论

20
在C++11中,这个表达式是被定义的,并且将导致i == 20
[expr.ass]/1可以得知:

在所有情况下,赋值都在右手和左手操作数的值计算之后进行排序,在赋值表达式的值计算之前进行。

这意味着赋值i+=1(i+=10)+=10的左手边值计算之前进行排序,该左手边值计算又排在对i的最终赋值之前。
在C++03中,这个表达式有未定义的行为,因为它导致i在没有中间序列点的情况下被修改两次。

3
这个回答只适用于C++11吗,还是适用于C++的所有版本? - Paul R
1
@PaulR:仅限C++11。C++03没有“sequenced after”的概念,而是使用序列点。实际上,在C++03中它是未定义的,因为在赋值之间没有中介序列点。 - user784668
1
谢谢 - 这应该在答案中得到澄清,因为问题没有指定C++11。 - Paul R
@PaulR 在 C++98 中,该段落说:“赋值操作的结果是在赋值发生后存储在左操作数中的值;”我不确定这是否在技术上意味着赋值之间存在一个序列点。至少我认为在 C++98 中这是未经明确说明的。 - bames53
或许值得一提的是,因为 i+=10 的结果是左值(即“i”对象本身),对 i 的修改不能被视为“独立”的副作用(在最终的“;”之前与任何东西都没有排序)。 - user396672
显示剩余3条评论

13

或许吧。这取决于C++的版本。

在C++03中,这是明显的未定义行为,因为赋值之间没有干预序列点。

在C++11中,正如Mankarse所解释的那样,这不再是未定义的行为了——括号内的复合赋值在外部赋值之前发生,所以没问题。


8
C++03的答案提出了一个新问题——如果修改其结果是未定义的,为什么赋值运算符在C++03中返回一个左值? - Mankarse
3
现在,这是一个很棒的问题。 - NPE
3
为了与简单的赋值保持一致,假设您想要一个 T&f,这样您就可以同时执行 f(x = 5)f(x += 5) - user784668
3
@Fanael:我知道你只是在猜测,但我不理解你的逻辑。如果赋值运算符不返回_lvalue_,那么既f(x = 5)也不会编译通过,也就是f(x += 5)同样如此(当f需要一个int&时)。这有什么不一致之处呢? - Mankarse
@Mankarse:对,我没有考虑到那个。在这种情况下,我认为我们应该坚持“没有人知道为什么”。 - user784668
5
@Mankarse 给出的理由(我并不完全同意)是为了支持像 int& f(int& i) { return i += 2; } 这样的东西。 - James Kanze

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接