运营商优先级与评估顺序

术语“运算符优先级”和“评估顺序”是编程中非常常用的术语,对于程序员来说是非常重要的。 而且,据我了解,这两个概念是紧密相连的。 谈论表情时,离不开对方。

让我们举一个简单的例子:

int a=1; // Line 1 a = a++ + ++a; // Line 2 printf("%d",a); // Line 3 

现在,显然第二Line 2导致未定义行为,因为C和C ++中的序列点包括:

  1. 在&&(逻辑AND)的左和右操作数的评估之间,|| (逻辑OR)和逗号运算符。 例如,在表达式*p++ != 0 && *q++ != 0 ,子表达式*p++ != 0所有副作用在任何尝试访问q之前完成。

  2. 在三元“问号”算子的第一个操作数和第二个或第三个操作数的评估之间。 例如,在表达式a = (*p++) ? (*p++) : 0 a = (*p++) ? (*p++) : 0在第一个*p++之后有一个序列点,这意味着它已经被执行第二个实例的时间增加了。

  3. 在完整的表达式结尾。 这个类包括for语句中的表达式语句(如赋值a=b; ),返回语句,if,switch,while或do-while语句的控制表达式以及所有三个表达式。

  4. 在函数调用中输入函数之前。 没有指定参数的评估顺序,但是这个顺序点意味着在输入函数之前所有的副作用都已经完成了。 在表达式f(i++) + g(j++) + h(k++)fi的原始值的参数调用,但是i在进入f的主体之前递增。 类似地, jk分别在进入gh之前被更新。 但是,并没有规定执行f()g()h()的顺序,也没有规定ijk的递增顺序。 因此, f体中的jk的值是不确定的。 3请注意,函数调用f(a,b,c)不是逗号运算符的使用, abc的评估顺序未指定。

  5. 在函数返回时,将返回值复制到调用上下文中。 (这个顺序点只在C ++标准中指定;它只是隐含在C中)

  6. 在初始化程序结束时; 例如,在声明int a = 5;评估int a = 5;

因此,通过点#3:

在完整的表达式结尾。 这个类包括for语句中的表达式语句(如赋值a = b;),返回语句,if,switch,while或do-while语句的控制表达式以及所有三个表达式。

Line 2显然会导致未定义的行为。 这显示了未定义行为如何与序列点紧密结合。

现在让我们再举一个例子:

 int x=10,y=1,z=2; // Line 4 int result = x<y<z; // Line 5 

现在看来, Line 5将使可变result存储1

现在第Line 5的表达式x<y<z可以被评估为:

x<(y<z)(x<y)<z 。 在第一种情况下, result的值将是0 ,在第二种情况下, result将是1 。 但是我们知道,当Operator PrecedenceEqual/SameAssociativity就起作用了,因此被评估为(x<y)<z

这就是在这个MSDN文章中所说的:

C操作符的优先级和关联性影响表达式中操作数的分组和评估。 只有其他具有更高或更低优先级的运算符存在时,运算符的优先级才有意义。 首先评估具有较高优先级的运算符。 优先级也可以用“绑定”来描述。 据说具有更高优先级的运算符具有更紧密的绑定。

现在,关于上面的文章:

它提到“具有更高优先级的运算符首先被评估”。

这听起来可能不正确。 但是,如果我们认为()也是一个运算符x<y<z(x<y)<z相同,我认为这篇文章并没有说错。 我的推理是,如果结合不起作用,那么完整的表达式评估就会变得含糊不清,因为<不是序列点

另外,我发现另一个链接说运营商的优先级和关联 :

此页面按优先顺序(从最高到最低)列出C运营商。 它们的关联性表示按照什么顺序应用表达式中相同优先级的运算符。

因此,第二个例子是int result=x<y<z ,我们可以看到在这里有3个表达式, xyz ,因为表达式的最简单的形式是由一个单独的文字常量或者对象。 因此,表达式xyz将是rvalues ,即分别为10,12 。 因此,现在我们可以将x<y<z解释为10<1<2

现在,由于现在我们有两个要评估的表达式,即10<11<2 ,因此联合性不起作用,并且由于运算符的优先级相同, 因此从左到右进行评估

以最后一个例子作为我的论点:

 int myval = ( printf("Operator\n"), printf("Precedence\n"), printf("vs\n"), printf("Order of Evaluation\n") ); 

现在在上面的例子中,因为comma运算符具有相同的优先级,所以表达式left-to-right计算,并且最后一个printf()的返回值存储在myval

J.1中的 SO / IEC 9899:201x 未指定的行为中提到:

除了为function-call(),&&,||,?:和逗号运算符(6.5)指定外,子表达式的评估顺序和副作用发生的顺序。

现在我想知道,这样说是错的吗?

评估顺序取决于运营商的优先级,留下未指定行为的情况。

如果我在我的问题中提到的某些内容出现错误,我想纠正。 我发布这个问题的原因是因为MSDN文章在我脑海中造成的混乱。 它是否错误

是的,MSDN文章是错误的,至少就标准C和C ++ 1而言

现在,评价顺序是由优先权决定的,不是! 就这么简单。 举个例子,让我们考虑你的例子x<y<z 。 根据关联规则,这个解析为(x<y)<z 。 现在,考虑在堆栈机器上评估这个表达式。 它完全可以做这样的事情:

  push(z); // Evaluates its argument and pushes value on stack push(y); push(x); test_less(); // compares TOS to TOS(1), pushes result on stack test_less(); 

这会在xy之前评估z ,但仍评估(x<y) ,然后将该比较的结果与z进行比较,就像它应该的那样。

总结:评估顺序与结合性无关。

优先顺序是一样的。 我们可以将表达式更改为x*y+z ,并且仍然在xy之前计算z

 push(z); push(y); push(x); mul(); add(); 

总结:评估顺序与优先级无关。

当/如果我们添加副作用,这仍然是一样的。 我认为将副作用看作是由一个单独的执行线程执行,并在下一个序列点(例如,表达式的结尾)进行join是很有教育意义的。 所以像a=b++ + ++c; 可以执行这样的事情:

 push(a); push(b); push(c+1); side_effects_thread.queue(inc, b); side_effects_thread.queue(inc, c); add(); assign(); join(side_effects_thread); 

这也说明了为什么显性依赖不一定会影响评估顺序。 尽管a是作业的目标,但评估bc 之前 ,仍然评估a 。 还要注意的是,尽管我已经把它写成了上面的“线程”,但它也可能是一个线程 ,并行执行,所以你不能保证一个增量与另一个增量的顺序。

除非硬件对线程安全队列提供直接(而且便宜的 )支持,否则这可能不会被用在真正的实现中(即使这样也不太可能)。 把一些东西放到一个线程安全的队列中通常会比单个增量花费更多的开销,所以很难想象有人会这样做。 然而从概念上讲,这个想法符合标准的要求:当你使用前/后增加/减少操作时,你指定了一个操作,这个操作会在评估完表达部分之后发生,并且将在下一个序列点。

编辑:虽然它不是完全线程化的,但是一些体系结构确实允许这样的并行执行。 对于几个例子,Intel Itanium和VLIW处理器(如某些DSP)允许编译器指定要并行执行的许多指令。 大多数VLIW机器都有一个特定的指令“包”大小,它限制了并行执行指令的数量。 Itanium还使用指令包,但在指令包中指定一位,以表示当前包中的指令可以与下一个包中的指令并行执行。 使用这样的机制,就可以得到并行执行的指令,就像在我们大多数人比较熟悉的体系结构上使用多线程一样。

总结:评估顺序与表观依赖性无关

在下一个序列点之前使用该值的任何尝试都会给出未定义的行为 – 特别是,“其他线程”在此期间(可能)正在修改该数据,并且无法使访问权与另一个线程同步。 任何使用它的尝试都会导致未定义的行为。

仅仅为了一个(当然,现在相当牵强的)例子,想象你的代码运行在一个64位的虚拟机上,但真正的硬件是一个8位的处理器。 当你增加一个64位变量时,它执行一个如下的序列:

 load variable[0] increment store variable[0] for (int i=1; i<8; i++) { load variable[i] add_with_carry 0 store variable[i] } 

如果你在这个序列的中间读取了某个值,那么只能修改一些字节,所以你得到的既不是旧值, 也不是新值。

这个确切的例子可能相当牵强,但是一个不太极端的版本(例如,一个32位机器上的一个64位变量)实际上是相当普遍的。

结论

评估顺序依赖于优先级,关联性,或(必然)依赖明显的依赖关系。 试图在表达式的任何其他部分中使用前/后递增/递减的变量确实会给出完全未定义的行为。 虽然实际的崩溃是不可能的,但你绝对不能保证得到旧的价值或新的价值 – 你可以完全得到别的东西。


1我没有检查过这篇文章,但是很多MSDN文章都讨论了微软的Managed C ++和/或C ++ / CLI,但是很少或者根本没有指出它们不适用于标准的C或者C ++。 这可能会导致他们声称自己决定适用于自己的语言的规则实际上适用于标准语言。 在这些情况下,这些文章在技术上并不是错误的 – 它们与标准的C或C ++没有任何关系。 如果您尝试将这些语句应用于标准C或C ++,则结果为false。

唯一的优先顺序影响评估的顺序是它创建依赖; 否则两者是正交的。 您已经仔细选择了一些简单的例子,其中由优先级创建的依赖关系最终会完全定义评估顺序,但通常情况并非如此。 不要忘记,许多表达式有两个作用:它们产生一个值,并且有副作用。 这两者不一定要一起出现,所以即使在依赖关系迫使一个特定的评估顺序时,这也只是价值评估的顺序。 它对副作用没有影响。

看这个的好方法是采取表达式树。

如果你有一个表达式,可以说x+y*z你可以把它重写成一个表达式树:

应用优先级和关联性规则:

 x + ( y * z ) 

应用优先级和关联性规则后,您可以放心地忘记它们。

以树形式:

  x + y * z 

现在这个表达式的叶子是xyz 。 这意味着你可以按任何你想要的顺序来评估xyz ,也就是说你可以以任何顺序评估*x的结果。

现在,由于这些表达式没有副作用,所以你并不在乎。 但如果他们这样做,排序可以改变结果,并且由于排序可以是编译器决定的任何东西,所以你有一个问题。

现在,顺序点为这个混乱带来了一点点的顺序。 他们有效地把树切成了几段。

x + y * z, z = 10, x + y * z

优先和相关性后

x + ( y * z ) , z = 10, x + ( y * z)

那个树:

  x + y * z , ------------ z = 10 , ------------ x + y * z 

树的顶部将在中间之前和中间之前评估。

它提到“具有更高优先级的运算符首先被评估”。

我只想重复我在这里所说的话。 就标准C和C ++而言,文章是有缺陷的。 优先级只影响哪个令牌被认为是每个运算符的操作数,但是它不以任何方式影响评估的顺序。

所以,链接只解释了微软如何实现的,而不是语言本身是如何工作的。

优先顺序与评估顺序无关,反之亦然。

优先规则描述了当表达式混合不同类型的运算符时,如何将不完整的表达式加括号。 例如,乘法的优先级高于加法,所以2 + 3 x 4相当于2 + (3 x 4) ,而不是(2 + 3) x 4

评估规则的顺序描述了评估表达式中每个操作数的顺序。

举一个例子

 y = ++x || --y; 

按照运算符优先级规则,它将被括起来( ++/--比优先级高于||优先级高于= ):

 y = ( (++x) || (--y) ) 

逻辑OR ||的评估顺序 (C11 6.5.14)

|| 运营商保证从左到右的评估。

这意味着左操作数,即子表达式(x++)将被首先评估。 由于短路的行为; 如果第一个操作数比较不等于0 ,则不计算第二个操作数 ,右边的操作数 – 尽管它的加括号大于(++x) || (--y) (++x) || (--y)

我认为这只是

 a++ + ++a 

epxression有问题,因为

 a = a++ + ++a; 

首先适合3.但是在6.规则:完成评估之前的任务。

所以,

 a++ + ++a 

得到一个= 1完全评估为:

 1 + 3 // left to right, or 2 + 2 // right to left 

结果是相同的= 4。

一个

 a++ * ++a // or a++ == ++a 

会有未定义的结果。 不是吗?

Interesting Posts