[c++11]Sequenced before 规则和求值顺序

任何 C++ 操作符的求值顺序都是 unspecified(后面提到的除外),unspecified 也就是标准没有指定,可以由编译器决定,这包括在函数调用表达式中函数参数的求值顺序以及任何表达式的子表达式的求值顺序。编译器可以按照任意顺序将它们求值,对于相同的表达式,编译器也可以选择不同的顺序将它们求值。

在 C++ 中,没有什么从左到右或者从右到左的求值顺序,只有操作符从左到右和从右到左的结合性。就比如说表达式f1() + f2() + f3() 会通过加法操作符从左到右的结合性被解析为 (f1() + f2()) + f3(),但是对 f3 的调用可能会最先执行,然后是 f1,最后是 f2,因为它们的求值顺序是 unspecified。

C++ 是一个注重效率的语言,标准不指定一些表达式的求值顺序就是为了让编译器能做尽可能多的优化,即便要牺牲掉例如 i=i++ 这样表达式的正确性(据说在 Java 中就没这些问题,这个表达式的结果是确定的)。

在 C++98/03 的标准中定义了 sequence point 来描述求值顺序,到了 C++11 中,用了更加清晰的 sequenced before 来描述它。下面要说的是 C++11 中的 sequenced before。

Definitions


在 C++ 中,对一个表达式或子表达式求值时有两种表现(两种可以同时存在)

  • side effect :对一个 volatile 的对象的访问(读或者写),对一个对象的修改(写),调用一个 I/O 函数或者调用一个有任何以上操作的函数。
  • value computation:对表达式返回值的计算。这可能包括对一个对象的确定(如果表达式返回的是对某个对象的引用),或者是对先前赋值过的对象值的读取(如果表达式返回一个数或者其它值)。

那么什么是 sequenced before 呢?下面是标准给出的定义。

Sequenced before is an asymmetric, transitive, pair-wise relation between evaluations executed by a single thread, which induces a partial order among those evaluations.

为了方便,后面我把 A is sequenced before B 记作 A < B,读作 A 先于 B,A is not sequenced before B 记作 A ! B。

给定两个求值 A 和 B,具体来说有这三种情况:

  • A < B:A 将在 B 之前执行
  • A ! B and B < A:B 将在 A 之前执行
  • A ! B and B ! A:有两种可能存在:
    • A 和 B 是 unsequenced:它们可能以任何顺序求值,并且可以重叠(在单个线程中,编译器可能插入包含 A 和 B 的指令)
    • A 和 B 是 indeterminably-sequenced:它们可能以任何顺序求值,但是不可以重叠,也就是说必然有 A 在 B 之前求值或者 B 在 A 之前求值。这个顺序有可能在下一个相同的表达式中相反。

Rules


这些规则你可以在 C++11 的标准上找到,最后的 references 也会给出是从哪里找来的。

  1.  和一个完整的表达式(full-expression)相关的 side effect 以及 value computation 都先于和下一个完整表达式相关的 side effect 以及 value computation 被求值(当一个表达式不是另一个表达式的子表达式,称这个表达式为完整的表达式)。
  2. 除了下面提到的以外,对一个运算符的运算数的求值、对一个表达式的子表达式的求值都是 unsequenced。
  3. 一个运算符的运算数的 value computation 先序于这个运算符结果的 value computation。
  4. 如果一个标量对象(比如 int 这样类型的对象)的 side effect 和另一个作用于相同对象的 side effect 或者 value computation 是 unsequenced,那么这个行为是 undefined。
  5. 当调用一个函数的时候,不管函数是不是 inline ,也不管是否使用的是显式语法调用(比如说 i + ji.operator + (j),这里 i 和 j 是某个重载了 operator + 的类型),这个函数的任一参数的表达式的 side effect 和 value computation,以及指定被调用函数的那个表达式(比如 (*func_ptr)() 中,*func_ptr 就是这个表达式)都先序于被调用函数体的所有表达式以及语句。但是不同参数的表达式的 value computation 和 side effect 都不存在序上的关系。
  6. 后自增运算符以及后自减运算符的 value computation 先序于它们的 side effect。
  7. 前自增运算符以及前自减运算符的 side effect 先序于它们的 value computation。这条和上一条规则就说明了为什么++i是先求值再返回而i++是先返回再求值。
  8. 内建(built-in)逻辑与运算符&&以及内建(built-in)逻辑或运算符||的第一个运算数(左边的那个)的 value computation 以及 side effect 先序于它们的第二个运算数(右边的那个)的 value computation 以及 side effect 。
  9. 条件运算符?:第一个表达式的 value computation 以及 side effect 先序于它第二或第三个表达式的 value computation 以及 side effect。
  10. 内建赋值运算符=以及所有内建复合赋值运算符的运算数(左右两个)的 value computation(但不是 side effect)先序于它的 side effect(即对左边运算数的修改),并且这个 side effect 先序于整个赋值表达式的 value computation(也就是说返回之前对象就修改完成了)。
  11. 内建逗号运算符,左边运算数的 value computation 以及 side effect 都先序于它右边那个运算数的 value computation 以及 side effect。
  12. 初始化列表的每一个元素的 value computation 以及 side effect 都先序于由逗号分隔、跟在它后面的元素的  value computation 以及 side effect。

Examples


就比如 i = i++ 这个表达式吧,i++ 的 side effect 以及赋值运算符的 side effect 是没有序上关系的(虽然说 i++ 的 value computation 是先序于赋值运算符的 side effect),因此这个行为是 undefined。这也就给了编译器优化的自由,它可能在之前保存了i的值,然后把i加一,之后把先前保存的值返回来修改i;也可能优化成直接用原来的值,之后加一。事实上这个表达式在不同编译器甚至是同一编译器上是有可能出现 1 和 0 两个结果的,但是有了上面的解释之后就不足为奇了。

References


  •  C++11 standard (ISO:IEC 14882:2011)
    • 1.9 Program execution (13, 14, 15)
    • 5.2.2 Function call [expr.call] (8)
    • 5.2.6 Increment and decrement [expr.post.incr] (1)
    • 5.3.4 New [expr.new] (16)
    • 5.14 Logical AND operator [expr.log.and] (2)
    • 5.15 Logical OR operator [expr.log.or] (2)
    • 5.16 Conditional operator [expr.cond] (1)
    • 5.17 Assignment and compound assignment operators [expr.ass] (1)
    • 5.18 Common operator [expr.common] (1)
    • 8.5.4 List-initialization [dcl.init.list] (4)
  • cppreference.com

Miskcoo's Space,版权所有丨如未注明,均为原创
转载请注明转自:http://blog.miskcoo.com/2014/07/cxx11-sequenced-before-and-evaluation-order

miskcoo

顺利从福州一中毕业!感觉大学周围都是聚聚十分可怕QAQ 想要联系的话欢迎发邮件:miskcoo [at] gmail [dot] com

Leave a Reply

Your email address will not be published. Required fields are marked *

NOTE: If you want to add mathematical formulas, use $$ to wrap them. For example, use $$x_0$$ to get $$x_0$$.

If you want to get a newline, hit Enter twice, that is, use double newlines to get a newline.