序列点从哪里来?

9

我知道写下这样的东西

++a = a++;

代码不仅难以阅读,而且违反了C/C++的序列点规则。

这些限制是从哪里来的?如何在发现它们之前就能看到这些“问题”?


那么,这是语言设计的限制还是机器(CPU)的限制,适用于任何语言? - Artur Marianek
5
哦,感谢你,贴心的编译器。错误:对 'a' 的操作可能未定义[-Werror=sequence-point]。 - chris
1
打开所有警告并编译,将警告视为逻辑错误处理,您就不必担心这个问题:-Werror -Wall -Wextra -ansi -pedantic - Martin York
@ArturMarianek:这是一个语言问题。语言可以决定特定的评估顺序,编译器必须强制执行。这与硬件无关。 - David Rodríguez - dribeas
4个回答

9

基本上,每个语句之间都有一个C++03序列点。更多信息请参见SO C++ FAQ。如果需要更多信息,请查阅C++标准,并记住,在C++11标准中,序列点被替换为“sequenced before”和“sequenced after”关系。

为避免问题,简单明了地在每个表达式中操作就可以了,不要试图太聪明,干预编译器的工作,留给编译器处理。你的工作是编写其他人能轻松理解的代码,即清晰的代码。多次更新和不必要的具有副作用的运算符使用与此不兼容。

提示:尽可能地使用const

这限制了读者必须考虑的可能状态变化。


感谢您的回答。我希望在这里能够“接受”许多答案 ;) - Artur Marianek

8
它们源自C或C++标准,该标准有效地列出了序列点1 在一些简单的情况下,编译器可能能够警告您正在调用未定义的行为,但在一般情况下则不会。

然而,如果您编写“有趣”的代码,例如您的示例,则通常只有违反序列点要求。 语言标准可以对此类代码施加特定约束(这就是像Java这样的语言所做的),但好处并不大,并且会导致防止某些类型的优化的潜在缺陷。


1. C++11中术语稍有改变,但原则基本相同,我想。


谢谢你的回答。我希望我能在这里“接受”很多答案 ;) - Artur Marianek
我之前在这个链接上回答了一个关于C++98和C++11之间区别的问题:https://dev59.com/DGgv5IYBdhLWcg3wXPot#10655884 - bames53

5

如何在发现问题之前看到这些“问题”?

以最严格的级别编译您的程序,并启用所有警告设置,以便将所有警告指出为错误。大多数主流编译器确实会指出由于序列点而导致的未定义行为错误。

使用gcc时,您可以使用:

-Wsequence-point

这将指出序列点问题。请注意,如果您使用-Wall,它将默认启用。

当然,最好的方法是尝试编写更易读的代码,避免序列点的错误。


2
@chris:-Wall 默认已经启用,就像我之前提到的那样。 - Alok Save
谢谢你的回答。我希望我能在这里“接受”很多答案 ;) - Artur Marianek
当你使用-Wall时,也会自动打开。 - Martin York

-1
这些限制是从哪里来的?在发现它们成为漏洞之前,如何看到这些“问题”?
这些限制源于执行过程中操作顺序的模棱两可或自由修改性。
以您的示例为例:
++a = a++;

该语言未定义在lvalue上的前缀递增应该在rvalue上的后缀递增之前还是之后进行。在这里添加一个约束会带来显著的成本;一般的好处是什么?这个示例代码应该明显的表现出哪种行为?

为了提高性能,编译器和/或处理器可以改变程序中的操作顺序(在严格限制下)。

过度限制执行顺序会:

  • 使在不同目标架构上实现C/C++更加困难
  • 增加语言规范的复杂性
  • 减少性能(为了获得什么收益?)
  • 阻止许多编译时优化,从而降低性能
  • 需要在执行期间禁用流水线和硬件重排序,进一步降低性能
  • 可能会破坏现有的代码

让我们看看如何限制不同代码示例的执行顺序:

a = b++ + ++c - --d - e--;

假设只有有限数量的寄存器可用,并且某些变量('d' 和 'e')在寄存器中,而有些则不在。 严格约束执行顺序(假设从左到右)可能需要:

  • 从寄存器中舍弃 'd'
  • 从寄存器中舍弃 'e'
  • 加载 'b'
  • 将其初始值保存为 'b,original'
  • 增加 'b' 的值(对于某些数据类型可能并非易如反掌)
  • 存储修改后的 'b'
  • 重新加载 'b,original'
  • 加载 'c'
  • 增加 'c' 的值
  • 保存更新后的 'c'
  • 将 'b,original' + 'c' 相加并保存为部分结果
  • 等等。

而如果允许例如先处理 'd' 和 'e',稍后再递增 'b',编译器可能能够显著减少步骤并提高性能。


该代码属于“未定义行为”,这意味着任何事情都有可能发生。它不是“无法确定是 [选项1] 还是 [选项2] 发生”。 - M.M
@M.M 是的,我们和编译器都认为 OP 的代码示例会具有未定义的行为。然而,OP 询问 的是,“如何在将它们视为错误之前看到这些‘问题’?” 我的意图是帮助读者了解为什么这是未定义的行为,为什么执行顺序没有详细指定,因为这种理解可以导致能够看到执行可能被重新排序的位置和发现这样的问题的能力。 - Technophile
我建议更改你的句子,包括“未定义是[选项1]还是[选项2]”。此外,在++a = a++的两种情况下,a都是一个左值,因此不应该说“在rvalue上进行后增量”,因为事实上后增量只能发生在左值上(对于内置类型)。 - M.M

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