在C99中,f()+ g()未定义或仅仅未指定?

时间:2010-10-16 22:09:18

标签: c c99 undefined-behavior sequence-points unspecified-behavior

我曾经认为在C99中,即使函数fg的副作用受到干扰,尽管表达式f() + g()不包含序列点,{{ 1}}和f将包含一些,因此行为将是未指定的:要么在g()之前调用f(),要么在f()之前调用g()。

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

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

3 个答案:

答案 0 :(得分:25)

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

即使编译器内联代码,它也必须遵守“似乎”规则 - 代码的行为必须与函数不交错时的行为相同。这限制了损坏的范围(假设没有错误的编译器)。

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


在评论中,supercat会问:

  

我希望源代码中的函数调用仍然作为序列点,即使编译器自己决定内联它们也是如此。声明为“内联”的函数是否仍然存在,或者编译器是否获得额外的纬度?

我相信'似乎'规则适用,并且编译器没有额外的宽容度来省略序列点,因为它使用明确的inline函数。认为(懒得去寻找标准中的确切措辞)的主要原因是允许编译器根据其规则内联或不内联函数,但程序的行为不应该改变(除了性能)。

  

另外,关于(a(),b()) + (c(),d())的排序可以说些什么? c()和/或d()是否可以在a()b()之间执行,或a()b()在{c()之间执行1}}和d()

  • 显然,a在b之前执行,c在d之前执行。我相信c和d有可能在a和b之间执行,尽管编译器生成类似代码的可能性很小;类似地,a和b可以在c和d之间执行。虽然我在'c和d'中使用'和',但这可能是'或' - 也就是说,这些操作序列中的任何一个都符合约束条件:

    • 绝对允许
    • ABCD
    • CDAB
    • 可能允许(保留≺b,c≺d订购)
    • ACBD
    • ACDB
    • CADB
    • CABD


    我认为这涵盖了所有可能的序列。另请参阅chat between Jonathan Leffler and AnArrayOfFunctions - 要点是AnArrayOfFunctions根本不认为允许“可能允许”的序列。

  

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

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

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

这假设b()d()没有关键的排序要求。

答案 1 :(得分:14)

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

答案 2 :(得分:1)

@dmckee

嗯,这不适合评论,但事情就是这样:

首先,你写一个正确的静态分析仪。在这种情况下,“正确”意味着如果对分析的代码有任何疑问,它将不会保持沉默,所以在这个阶段你快乐地混淆未定义和未指定的行为。它们在关键代码中都是坏的和不可接受的,你正确地警告它们。

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

所以你想为

发出一个警告
*p = x;
y = *p;

因为只要p是第一个语句的有效指针,就可以假定它是第二个语句的有效指针。并且不推断这会降低您对精确度量的评分。

所以你教你的分析器假设p是一个有效的指针,只要你第一次在上面的代码中警告它,这样你就不会第二次发出警告。更一般地说,您学习忽略与您已经警告的内容相对应的值(和执行路径)。

然后,您意识到没有多少人在编写关键代码,因此您可以根据初始正确分析的结果为其余代码进行其他轻量级分析。比方说,一个C程序切片器。

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

用户乐意忽略警报并使用切片器。

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

所以不应该对警报的含义有任何误解,如果有人想忽略它们,只应排除明确无误的行为。

这就是你最终对识别未指明行为和未定义行为的强烈兴趣。没有人可以责怪你忽视后者。但是程序员会在不考虑它的情况下编写前者,当你说你的切片器排除了程序的“错误行为”时,他们就不会感到他们感到担心。

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