序列点和部分顺序

23
几天前,这里讨论了表达式i = ++i + 1是否会引发未定义行为(Undefined Behavior)。
最终得出结论:由于'i'的值在两个序列点之间变化了多次,因此它会引发未定义行为。
我曾在同一帖子中与Johannes Schaub进行过讨论。他认为: i=(i,i++,i)+1 ------ (1) /* 也会引发未定义行为 */ 我则认为(1)不会引发未定义行为,因为逗号操作符','在i和i++之间以及i++和i之间清除了前面子表达式的副作用。
然后他给出了以下解释:
“是的,i++后的序列点会在它之前完成所有副作用,但没有什么能阻止赋值的副作用与i++的副作用重叠。根本问题在于赋值的副作用未指定发生在赋值操作数的评估之前还是之后,因此序列点无法保护这一点:序列点引入了一个部分顺序:仅仅因为i++之前和之后有一个序列点并不意味着所有副作用都按照i排序。
此外,请注意,仅仅一个序列点并不代表什么:代码的形式并不能决定评估的顺序。它由语义规则来决定。在这种情况下,没有语义规则说明赋值的副作用何时发生与其操作数或这些操作数的子表达式的评估相关。”
粗体字部分的陈述让我困惑了。据我所知:
“在执行序列中的某些指定点(称为序列点),先前评估的所有副作用都应该完成,并且随后的评估不应该产生任何副作用。”
既然逗号运算符也指定了执行顺序,当我们到达最后一个i时,i++的副作用已经被取消了。如果评估顺序未指定,则Johannes可能是正确的(但在逗号运算符的情况下,它已经很好地指定了)。
因此,我只想知道(1)是否会引起UB。有人能给出另一个有效的解释吗?
谢谢!

9
我认为我和litb的想法一致,但我必须问一句:“你为什么关心这件事?”没有经验的C或C ++程序员不会写出那样的代码。 - anon
4
坦白地说,我不在乎(我知道写C/C++代码时应避免使用这种表达方式),但这只是为了解决我的疑惑。没有冒犯之意。 :-) - Prasoon Saurav
25
这种细节非常深奥,说实话我也不相信编译器的作者们已经完全弄清楚了它们。 - Crashworks
6
我同意 - 这个问题希望实际上没有太多的用处。但有时候人们只是对这些细节感到好奇,这并不一定有害(除非有人开始像这些例子一样编写代码)。 - Michael Burr
为什么不坚持编写更简单、更清晰的代码,比如 int i=0,j; j=i+(i++); Stroustrup 的易于记忆的规则(不超过一次对变量的写入)说这是可以的。(只是开个玩笑) - mlvljr
3个回答

11

C标准关于赋值运算符(C90 6.3.16或C99 6.5.16 赋值运算符)的规定如下:

更新左操作数存储值的副作用应在前一个和下一个序列点之间发生。

我认为在以下语句中:

i=(i,i++,i)+1;

赋值运算符之前的“previous”序列点将是第二个逗号运算符,“next”序列点将是表达式末尾。因此,我认为该表达式不会引起未定义行为。

然而,这个表达式:

*(some_ptr + i) = (i,i++,i)+1;
在这种情况下,使用赋值运算符的两个操作数的评估顺序是未定义的,因此会导致未定义行为。问题不在于赋值运算符的副作用何时发生,而在于您不知道左侧操作数中使用的i的值是在右侧操作数之前还是之后进行评估的。在第一个示例中不会出现这种评估顺序问题,因为在该表达式中实际上并未使用i的值作为左操作数 - 赋值运算符只关心i的“左值-性”。但我认为所有这些都够棘手(并且我对涉及的细微差别的理解也够棘手),以至于如果有人能说服我改变看法(无论哪一方面),我也不会感到惊讶。

3
确实,yeah *(some_ptr + i) = (i,i++,i)+1; 是未定义行为。它的行为类似于 a[i]=++i,也会导致未定义行为。 - Prasoon Saurav
2
这是正确的,第二个逗号在增量和i的最后一次读取之间引入了一个序列点,它们都在对i的赋值之前有序。由于没有可能在涉及写操作的操作之后进行涉及读操作的操作而不使用序列点,因此表达式是明确定义的。顺便说一下,i =(i ++,i ++,i)+1也可以(第一个增量在第二个增量之前排序,其余与原始示例相同)。这个问题在http://www.open-std.org/JTC1/SC22/wg14/www/docs/n926.htm中得到了很清楚的讨论。 - mlvljr
@mlvljr - 感谢您指向 N926;我需要几天时间仔细阅读。 - Michael Burr
我刚刚注意到我的同事屏幕上的这个线程 :) “分配运算符之前的序列点将是第二个逗号运算符” -> 对我来说,“previous”序列点是赋值表达式之前的序列点。也就是说,在“a; a =(b,c); d”中,“previous sequence point”是第一个“;”,下一个序列点是第二个“;”。不过我并不确定。但我相信在C++0x中,这绝对不是未定义的行为 :) - Johannes Schaub - litb
我刚想起来usenet上有一个非常长的帖子 :) (请参见Michael Foukarakis下面的链接)。 我们当时得出结论,在C1x和C++0x中可以这样做,但是我们没有对C99 / C89和C++03达成结论,而且我仍然认为它们会导致未定义行为。 但无论如何,既然我现在记得那个帖子很长,我们没有得出有关pre-C++0x的明确结果,我认为重新在这里讨论没有意义 :) - Johannes Schaub - litb
@litb:我绝对不会声称对这些内容有任何确定的看法。正如在问题的评论中所提到的那样,这主要是学术性的东西,老实说,过多地思考序列点开始让我感到头痛(这表明我正在学习什么?)。我可能需要开始监视usenet上的comp.lang.c等论坛,以尝试更好地了解标准的演变(和历史),但这将需要等待一些其他事情在现实生活中稳定下来。 - Michael Burr

4

i=(i,i++,i)+1 ------ (1) /* invokes UB as well */

这并不会引起未定义的行为。 i++ 的副作用将在下一个序列点之前发生,该序列点由其后面的逗号表示,并且也会在赋值之前发生。

不过这是一种有趣的语言谜题。 :-)

编辑:这里有一个更详细的解释(链接)


好吧,如果标准不能说服他,我不确定什么能。 - Michael Foukarakis
顺便问一下,有没有一个负责“序列点”问题措辞和语义的标准人员,我们可以给他发送一封友好的[社区?]信件,询问以下内容:a)原理,b)推理算法,c)阅读建议?那将非常方便。 - mlvljr
1
@mlvljr:我认为他的名字是Peter Seebach(又名Seebs),你可能会在那个帖子中找到他的一些帖子。 :) - Prasoon Saurav
似乎他并不是真正喜欢标准的事情 - 比如,这个人说他对某些序列点方面感到不确定。顺便说一下,有一份官方文件涉及(在其他方面之间)这个问题 - http://www.open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf。 - mlvljr
1
如果我错了,请纠正我。那个线程只是发现它在 C1x 和 C++0x 中是有效的。但是他们没有证明在 C++03 或 C89/C99 中不会引发未定义的行为。 - Johannes Schaub - litb
显示剩余4条评论

3

我认为下面的表达式肯定存在未定义行为。

i + ((i, i++, i) + 1)

原因在于逗号运算符在括号中的子表达式之间指定了序列点,但并未指定+左操作数的计算在该序列的哪个位置发生。一种可能性是在围绕i++的序列点之间,这违反了5/4,因为i被写入两个序列点之间,但在同一序列点之间被读取两次,不仅用于确定要存储的值,还用于确定+运算符的第一个操作数的值。

这也具有未定义行为。

i += (i, i++, i) + 1;

现在,我对这个说法并不十分确定。
i = (i, i++, i) + 1;

尽管相同的原则适用,但必须将i评估为可修改的lvalue,并且可以在任何时候进行评估,但我并不确定其值是否作为此过程的一部分被读取。(或者有没有其他限制违反表达式导致UB?)
子表达式(i, i ++,i)作为确定要存储的值的一部分发生,并且该子表达式在将值存储到i后包含序列点。 我看不出这不需要完成i ++的副作用才能确定要存储的值,因此赋值副作用可能发生的最早时间是这个序列点之后。
在此序列点之后,i的值最多被读取一次,仅用于确定将存储回i的值,因此最后一部分是正确的。

1
UB只是不能保证某些东西能按预期工作,对吧?因为所有这些语句都可以正确编译并且不会崩溃(而且我已经测试过它们很多次)。 - the_drow
3
如果某个东西存在UB,那就意味着它没有真正的“按预期”行为。表达式可能会做你想要的事情,但不能保证在某个符合规范的实现中发生完全不同的事情。 - CB Bailey
这篇论文实际上涉及到C语言,体现了委员会成员对问题的看法,并在最后一部分声明:“第5节列出的规则是标准的解释。我们希望得到一种解释,能够给出一组明确定义的表达式,以满足我们(作为程序员)的期望。在这方面,标准在浮点标志、易失性和信号处理方面不清楚。标准在描述函数调用方面也不足。尽管DR087建议函数调用不重叠,但标准并没有明确讨论这一点。” - mlvljr
所以[C语言]标准似乎只是不清楚(可能C++也是如此)。至少,有人正式证明了它们中的任何一个的一致性吗? :) - mlvljr
1
@Charles - 关于“i必须被评估为可修改的lvalue”的问题:标准说,“除非它是......点运算符或赋值运算符的左操作数,否则没有数组类型的lvalue将转换为存储在指定对象中的值(并且不再是lvalue)。”这意味着赋值运算符的左操作数不会转换为存储的值,因此值不会“读取”。操作数仍然是一个lvalue,它是一个对象类型(它指代对象)。 - Michael Burr
显示剩余3条评论

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