括号是否强制(当前)C中的评估顺序?

时间:2018-10-12 23:10:59

标签: c language-lawyer parentheses operator-precedence

我希望有人可以引用最新的C标准的章节和经文。我以为它在那里,而我一直找不到。

在过去,C语言定义专门允许编译器即使在有括号的情况下也可以评估关联等效表达式,甚至 。因此,源语句

a = (b + c) + d;

实际上可以通过将c和d相加,然后将b与该结果相加来求值。 (请参阅:K&R,第1版,第2.12节,第49页)。该措辞在第二版中已删除,但 not 并没有专门说表达式必须被视为带括号。我的理解是,这是引入“一元+” hack的部分原因:在陈述中,“ a = +(b + c)+ d;”一元加号将强制评估(b + c)。或者,可以依靠序列点的定义,并使用多个语句:

tmp = b + c;
a = tmp + d;

并希望过度积极地进行优化的编译器进行正向替换不会使事情搞砸。

我听说它声称如果当前的C标准不再适用这种情况,并且在子表达式评估期间会考虑括号。我还没有找到用实际标准语言表达的明确声明。特别是,该标准表示类似在带括号的子表达式后有一个序列点(这可能是一个过于严格的坏主意,但会清楚地定义评估)。

4 个答案:

答案 0 :(得分:3)

该标准的相关部分为C11 5.1.2.3 "Program execution"

总而言之,C是根据产生可观察到的行为的抽象机定义的,该定义可以在该节的第6点中看到。 (基本上是输出)。只要生成的可观察行为与抽象机根据语言规范为执行程序而产生的可观察行为相匹配,编译器就可以对符合标准的程序执行其喜欢的任何事情。

在您的示例中,添加一元+对可观察的行为没有影响,因此编译器可以忽略它。

在此特定示例中,编译器可以对加法进行重新排序,因为它知道对多个int操作数进行加法会产生相同的结果,而与排序无关(如果“导致未定义的行为”被视为相同的结果,主要命令就可以做到。

答案 1 :(得分:0)

另一个答案指出,只要程序的可观察到的行为与为抽象机定义的行为相匹配,就可以在执行细节方面允许C实现。尽管这是真实且相关的,但仍有可能给人留下错误的印象。

在许多情况下,C实现无法在编译时完全确定在不更改可观察行为的情况下可以容纳哪些与抽象机行为的偏差。造成这种情况的原因有很多,其中

  • 程序分布在多个翻译部门
  • 程序依赖于仅在运行时确定的值
  • 可通过指针/可能的指针别名访问对象
  • 长/复杂的依赖链

因此,不应将实现自由(其正式定义主要是为了使实现得以优化)解释为一揽子许可,以便重新排序评估意愿。在实践中,至少在默认情况下,现代C实现对于避免可能会更改可观察行为的优化非常可靠。由于现代CPU可以自行决定执行或执行执行或命令执行,因此对于完全符合标准的实现来说,这是相当不错的。

假设实现确实符合要求,那么问题不应该是表达式评估实际上是否 重新排序,而是C是否允许它出现进行重新排序。也就是说,抽象机器行为的要求似乎与我最相关。为此,标准中最相关的部分是section 6.5

尤其是:

  

对运算符的操作数的值计算进行排序   在运算符结果的值计算之前。

  

运算符和操作数的分组由语法指示。

给出示例表达式

a = (b + c) + d;
然后,

指定首先评估子表达式(b + c)d,然后计算它们的总和。该标准的早期版本具有相似的措辞,尤其是与后者的措辞类似,并且我认为由该标准的每个版本定义的抽象机行为要求相同是毫无争议的。

如果您无法分辨出差异(达到可观察到的行为的极限),那么您真的在乎执行什么重新排序?您可能会这样做的原因有很多,例如执行时间是最大的原因,但是您不必过多担心正确性。

答案 2 :(得分:0)

需要C实现才能产生源代码中表示的结果。它可以使用其选择的任何计算来产生此结果。为此,程序的结果就是C标准定义为可观察到的行为

  • 将数据写入文件。
  • 交互式设备的输入和输出动态。
  • 访问易失性对象。

如果源代码评估(a+b)+c并打印出来(这样结果是可观察到的行为),则C实现必须产生与添加ab相同的结果然后添加c,但不需要通过添加ab然后添加c来获得该结果。

但是,C标准允许比其标称类型以更高的精度和指数范围对浮点表达式求值(例如,double算术可用于仅包含float个操作数的表达式) ,并且未指定浮点算术和库例程所需的精度。如果您担心浮点表达式的精确行为,那么您不仅要考虑求值顺序,还需要考虑其他因素。您必须考虑使用的C实现的质量和属性。

此外,某些“ C”编译器在浮点行为方面也不遵循C标准。同样,您必须考虑使用的C实现的质量和属性。

C标准要求强制转换或分配以“去除”多余的精度。因此,如果您写:

t = a+b;
printf("%.99g\n", t+c);

然后,编译器必须产生结果,好像(a+b)+c已被评估到某种精度,然后四舍五入到其标称类型,然后添加了c。这可能会产生双舍入误差,因为a+b的计算精度过高,因此舍入到名义类型的后续舍入会产生与仅在名义类型中添加a+b不同的结果

总而言之,如果要精确控制浮点算术,则不能依赖C标准。您必须向特定的编译器寻求保证(例如,仅使用名义类型的编译器开关)或使用另一种编程语言。

答案 3 :(得分:0)

感谢所有答复。我本来希望该标准在这一点上更加明确,但我想不是。我将以我的Kahan求和算法的规范示例作为具体实例,总结从这次讨论中得出的结论:

请考虑以下内容(err,item和totalSum为“ double”类型):

err = ((nextItem + totalSum) - totalSum) - nextItem;

在这里,我们处于循环中,总结了一组项目的元素。 如果以上语句实际上完全按照书面形式“ err”执行 将包含否则将“掉头”的位 精度有限(totalSum与nextItem相比较大)。

C抽象机要求对表达式进行求值 “假设”是按书面形式计算的。 C通常允许中间 要以更高的精度评估表达式,然后是最终 结果四舍五入以适合。不幸的是,C抽象机 具有无限的精度。这意味着它合法/符合 实现将上述表达式优化为:

err = 0.0;

因为,如果您具有无限的精度,它们将是相同的。

特定实施方式可以选择实施更严格的规则 关于表达式评估(以及所有真实的实现) 这样做),例如要符合IEEE 754语义,但这不是 C标准必需

但是,标准确实要求 或强制转换,必须将有问题的值转换为正确的值 在这一点上键入。在实践中,这打破了无限精度 范例。因此,如果我们这样写表达式:

double tmp = nextItem + totalSum;
tmp = tmp - totalSum;
err = tmp - nextItem;

在这里,我们要做的标准保证人, 因为当我们将子表达式分配给tmp时,该值必须为 四舍五入以适合,因此不允许取消正向替换 排除条款。我们甚至可以这样做:

err = ((double)((double)(nextItem + totalSum)) - totalSum) - nextItem;

看起来很奇怪,因为所有变量都已经是double了, 但是强制转换迫使编译器考虑有限的影响 优化过程中的精度。

一个特定的实现实际上是否真的是 关于这一点的标准符合性是另一个问题。