54

我曾经认为在 C99 中,即使函数的副作用fg干扰,虽然表达式f() + g()不包含序列点,f并且g会包含一些,所以行为是未指定的:要么 f() 会在之前调用g() 或 g() 在 f() 之前。

我不再那么肯定了。如果编译器内联函数(即使未声明函数,编译器也可能决定这样做inline)然后重新排序指令怎么办?可以得到与上述两者不同的结果吗?换句话说,这是未定义的行为吗?

这不是因为我打算写这种东西,这是为了在静态分析器中为这样的语句选择最佳标签。

4

3 回答 3

25

表达式f() + g()至少包含 4 个序列点;调用前一f()(在评估其参数的所有零之后);调用前一g()(在评估其参数的所有零之后);一个作为返回的调用f();和一个作为返回的调用g()。此外,与 相关联的两个序列点f()都出现在与 相关联的两个序列点之前或之后g()。您无法确定序列点将按哪个顺序出现 - f 点是否出现在 g 点之前,反之亦然。

即使编译器内联代码,它也必须遵守“好像”规则——代码的行为必须与函数没有交错一样。这限制了损坏的范围(假设编译器没有错误)。

因此,评估f()和的顺序是未指定的。g()但其他一切都很干净。


在评论中,supercat问道:

我希望源代码中的函数调用保持为序列点,即使编译器自行决定内联它们。对于声明为“内联”的函数是否仍然如此,或者编译器是否获得了额外的自由度?

我相信“好像”规则适用,并且编译器没有额外的自由度来省略序列点,因为它使用了显式inline函数。认为(懒得在标准中寻找确切措辞)的主要原因是允许编译器根据其规则内联或不内联函数,但程序的行为不应该改变(除了表现)。

另外,关于 的排序可以说什么(a(),b()) + (c(),d())?是否有可能 for c()and/ord()a()and之间执行b(),或者 for a()or在andb()之间执行?c()d()

  • 显然,a 在 b 之前执行,c 在 d 之前执行。我相信 c 和 d 可以在 a 和 b 之间执行,尽管编译器不太可能生成这样的代码;类似地,a 和 b 可以在 c 和 d 之间执行。虽然我在 'c and d' 中使用了 'and',但它可能是一个 'or' - 也就是说,这些操作序列中的任何一个都满足约束:

    • 绝对允许
    • A B C D
    • cdab
    • 可能允许(保留 a ≺ b, c ≺ d 顺序)
    • 无名氏
    • 数据库
    • 计算机辅助数据库
    • 出租车

     
    我相信这涵盖了所有可能的序列。另请参阅Jonathan Leffler 和 AnArrayOfFunctions 之间的聊天——要点是AnArrayOfFunctions根本不认为“可能允许”的序列是允许的。

如果这样的事情是可能的,那将意味着内联函数和宏之间存在显着差异。

内联函数和宏之间存在显着差异,但我不认为表达式中的顺序是其中之一。也就是说,任何函数 a、b、c 或 d 都可以用宏代替,并且可以出现相同的宏体顺序。在我看来,主要区别在于,对于内联函数,函数调用(如主要答案中所述)以及逗号运算符都有保证的序列点。使用宏,您会丢失与功能相关的序列点。(所以,也许这是一个显着的差异......)然而,在很多方面,这个问题更像是关于有多少天使可以在针头上跳舞的问题——这在实践中并不是很重要。如果有人向我展示了这个表情(a(),b()) + (c(),d())在代码审查中,我会告诉他们重写代码以使其清楚:

a();
c();
x = b() + d();

并且假设对b()vs没有关键的排序要求d()

于 2010-10-16T23:02:05.267 回答
14

序列点列表见附录 C。函数调用(所有被评估的参数和传递给函数的执行之间的点)是序列点。正如您所说,未指定首先调用哪个函数,但是这两个函数中的每一个都会看到另一个函数的所有副作用,或者根本没有。

于 2010-10-16T22:28:42.297 回答
1

@dmckee

好吧,这不适合评论,但事情是这样的:

首先,您编写一个正确的静态分析器。在这种情况下,“正确”意味着如果分析的代码有任何可疑之处,它不会保持沉默,所以在这个阶段你可以愉快地将未定义和未指定的行为混为一谈。它们在关键代码中既糟糕又不可接受,您正确地警告它们两者。

但是您只想对一个可能的错误发出一次警告,而且您知道与其他可能不正确的分析器相比,您的分析器将在基准测试中根据“精度”和“召回”来判断,所以你不能对同一个问题发出两次警告......无论是真警报还是假警报(你不知道哪个。你永远不知道哪个,否则太容易了)。

所以你想发出一个警告

*p = x;
y = *p;

因为只要p在第一条语句中是一个有效指针,就可以在第二条语句中假定它是一个有效指针。并且不推断这会降低您在精度指标上的分数。

因此,一旦您在上面的代码中第一次警告它,您就教您的分析器假定它p是一个有效的指针,这样您就不会第二次警告它。更一般地说,您学会忽略与您已经警告过的事物相对应的值(和执行路径)。

然后,您意识到编写关键代码的人并不多,因此您根据最初的正确分析结果对其余代码进行其他轻量级分析。比如说,一个 C 程序切片器。

然后你告诉“他们”:你不必检查第一次分析发出的所有(可能经常是错误的)警报。切片程序的行为与原始程序相同,只要它们都没有被触发。切片器生成与“已定义”执行路径的切片标准等效的程序。

用户可以愉快地忽略警报并使用切片器。

然后你意识到也许有一个误解。例如,memmove当使用不指向同一块的指针(比较不指向同一块的地址)调用时,大多数实现(您知道,处理重叠块的实现)实际上会调用未指定的行为。并且您的分析器忽略了两条执行路径,因为两者都未指定,但实际上两条执行路径是等效的并且一切都很好。

因此,不应该对警报的含义有任何误解,如果打算忽略它们,则应仅排除明确无误的未定义行为。

这就是您最终对区分未指定行为和未定义行为产生浓厚兴趣的方式。没有人可以责怪你忽略了后者。但是程序员会想都没想就写前者,当你说你的切片器排除了程序的“错误行为”时,他们不会感到担心。

这是一个绝对不适合评论的故事的结尾。向读到这么远的人道歉。

于 2010-10-17T03:21:04.713 回答