C++ - 表达式求值顺序

来源:互联网 发布:水星 访客网络 编辑:程序博客网 时间:2024/04/30 21:03

在C++里,表达式求值顺序一直是一个大坑,这是由于为了给编译器更大的优化空间,C++对表达式的求值做了许多非常灵活的规定(其实就是不规定,编译器愿意怎么实现都可以)。这些灵活的规定也给C++带来了许多在其它语言中不存在的未定义行为(undefined behavior),比如i=i++,甚至有一些是标准委员会都没有预想到。

在C++03里,表达式的求值顺序依靠序列点(sequency point)来定义。有关序列点的介绍,在 这里 和 这里 可以见到。但是其中也提到了序列点所存在的一些问题。到了C++11,标准委员会放弃了序列点,而使用了一种新的方式定义表达的求值顺序。两种定义的结果大同小异,这里使用C++11的定义方式,介绍一下C++中的表达式求值。

C++11中还引入线程的概念,不过这里讨论的是同一线程中表达式的求值顺序,不讨论不同线程之间的顺序与同步的问题。

一些定义

side effect 副作用

访问(读或写)volatile的变量,修改一个对象(变量),调用系统I/O函数,或调用会发生以上三种行为函数,统称为副作用。对表达式的求值,不只包括求出表达式值(即返回值,value computation),还包括完成表达式中的包含的各种副作为。

int i=1;i++; // 对其求值包括两部分,计算返回值(1),以及其副作用(将i赋值为2)

full-expression 完整表达式

一个完整式指不是其它表达式子表达式的表达式。

一个表达式可以包含子表达式,也可以成为其它表达式的一部分,如(y=i+1)!=0; 中,y=i+1 包含子表达式 i+1 ,同时它是整个表达式(y=i+1)!=0的一个子表达式。在这里(y=i+1)!=0就是一个完整表达式。

一个完整表达式包括根据表达式所在上下文成生成类型转换,以及临时变量的析构。

struct S {S(int i): I(i) { }  int& v() { return I; }private:  int I;};void f() {  if (S(3).v()) // if 内部部分构成一个完整表达式。   // 在函数求值后的左值到右值的转换(int& --> int),以及 int --> bool 的转换(由于其出现在if条件判断中,所以需要转换为 bool),   // 以及S(3)临时变量的析构都是这一完整表达式的一部分。  { }}
完整表达式还会包含一些表面上没有出现在这个表达式中的内容,如函数调用表达式中,默认参数的计算是完整表达式的一部分,虽然默认参数并不出现在这个表达式中。

undefined behavior 未定义行为

标准不对程序的结果作为任何规定,程序可以出现任何结果(包括程序异常中止)。同时,程序的两次运行可以出现不同的结果。当包括未定义行为时,同一个程序中的相同代码也可能得到不同结果。

int i=0;printf("%d,%d", i++, i++); // 这是 C++ 里一个非常著名的考试题,也是一个非常著名的未定义行为,也就是说,它的结果是不确定的。

C++11中关于表达式求值顺序的定义方法

对于两个求值(返回值计算、副作用、也可能两者同时存在)A和B,C++11使用以下一些关系定义A与B的顺序:

sequenced before : A先于B。A中的任何计算都先于B中的任何一个计算。即B开始的时候A已经完全结束。

sequenced after : A后于B。即B先于A。

unsequenced : 无顺序的。A与B发生的顺序不一定,并有可能交差。即有可能A执行到一半的时候开始B的执行。

indeterminately sequenced : 顺序未指定。A先于B 或 B先于A。编译器可以选择任何一个,但是A与B的执行不能交差。


注意,对于无顺序的(unsequenced)和顺序未指定(indeterminately sequenced)的求值,标准并不要求两次不同求值使用相同的顺序。


表达式求值顺序

1

一个完整表达式的求值与副作用 先于(sequenced before) 下一个完整表达式的求值与副作用。

即两个不同的完整表达式的求值之间的顺序是确定的,不会交叉。

2

除非有特别说明(见5~10),一个运算符的不同操作数的求值是无顺序的(unsequenced),一个表达式中的不同子表达式的求值是无顺序的(unsequenced)。

运算操作数的值计算(value computation)先于(sequenced before)运算符结果的值计算(value computation)。

int i = 1, j = 1, k = 1, n = 1;(i+j)*(k+n+j);// i+j 与 k+n+i 的值计算是无顺序的。编译器可以选择先计算k+n,再计算i+j,然后计算k+n+i。// 但是 i+j 的计算与 k+n+j 的计算都先于两者相乘结果的计算。

3

如果对同一个简单对象(scalar object,比如基本数据类型的变量)的两个不同的副作用是无顺序的,那么这是一个未定义行为。

如果对一个简单对象的一个副作用与另一个需要此对象值的值计算是无顺序的,那么这是一个未定义行为。

int i = 0;i++ + i++;// + 的两个操作数各有一个对 i 的副作用(写 i 的值),但是它们是无顺序的,          //  因此这是一个未定义行为。(结果不确定)i + i++; // + 的左操作数(i)是一个需要 i 的值计算(即需要计算i的值),右侧(i++)包含一个对 i 的副作用,两者是无顺序的,         // 因此这也是一个示定义行为。(结果不确定)

4

函数调用时,任何实参的求值及副作用先于 函数体的任何一条语句及表达式求值。

函数的不同实参的求值与副作用是无顺序的。

int func(int, int);int i = 0;func(i++, i++); // 未定义行为,函数实参的求值与副作用是无顺序的,而它的两个实参求值各包含一个对 i 的副作用。

在调用函数中,任何即不先于被调用函数,也不后于被调用函数的求值,它们与被调用函数都是顺序未指定的(indeterminately sequenced)。即,调用函数中的任何求值与被调用函数不交差。

int foo(int);int i = 0, j = 0, k = 0;(i++ + k) + foo(j++); // i++ + k 与 foo(j++) 是无顺序的,但是i++ + k 中的每一个求值(自增与求和)与foo的函数体的执行是顺序未指定的,                      // 即它们不会插入到函数执行中。注意先自增(i++),再调用函数,最后求(i++ +k)是可能的。(i++ + k) + foo(i++); // i++ + k 与 foo 是函数体是顺序未指定的,但是 i++ + k 与 i++ (foo的实参)的求值仍然是无顺序的,                      // 这是一个未定义行为,结果不确定。

在进行函数体之前,要由实参对形参进行初始化。对同一个函数调用的不同形参的初始化是顺序未指定的(indeterminately sequenced)。

注意,这里所说的函数调用并不需要显示的函数调用的形式。例如,运算符重载、构造说析构、类型转换等等,都可以在没显示函数调用形式的情况下发生函数调用。

5 自增自减 i++ i-- ++i --i

对于后缀形式 i++ i--

表达式的值计算(value computation)先于对变量的修改(side effect)

与其顺序未指定的函数调用不能查入到该值计算与变量修改之间

对于前缀形式 ++i --i

返回被更新之后的操作数。注意返回的不是操作数的值,而是操作数本身,从而他是一个左值。

当 i 不为 bool 是,++i --i 分别与 i+=1 i-=1 等价。

6 operator new

allocator function (内存分配函数)与初始化参数的求值是顺序未指定的。

对新建的对象初始化先于对new表达式的值计算

7 && || (逻辑与,逻辑或)

当 && 的左操作数值为 false ,或 || 的左操作数值为 true 时, 它们的右操作数不会被求值。

如果这两个运算符的两个操作数都需要被求值时,左操作数的值计算与副作用 先于 右操作数的值计算与副作用。

8 ?:

?: 运算符有三个操作数,其中第二个与第三个操作数只有一个会被求值。当第一个操作数值为 true 时,第二个操作数被求值;当第一个操作数值为 false 时,第三个操作数被求值。

对第一个操作数的值计算与副作用先于第二个或第三个操作数的求值。

9 = *= /= %= += -= >>= <<= &= ^= |= (赋值运算符)

赋值操作后于其左右操作数的值计算(value computation),先与赋值操作,同时赋值操作先于赋值表达式的值计算。

i = i++ // 未定义成为。赋值操作(对 i 的一个副作用)后于 i++ 的值计算。        // 但是,i++ 中对 i 的副作用与赋值操作(对 i 的另一个副作用)是无顺序的,因而这是一个未定义成为。

注意赋值表达式返回他的左操作数的一个左值。求值顺序个规定保证这个被返回的左操作数是已经被赋值过的。

对复合赋值运算符 E1 op= E2 ,其求值包括 计算 E1 op E2 ,将结果赋值给 E1 ,返回 E1 。任何函数调用不能插入以上步骤中。

10 , (逗号运算符)

左操作数的值会被丢弃。

左操作数的值计算与副作用 先于 右操作数的值计算与副作用。

注意 “,” 还会被用于分隔,要区分其与逗号运算符的不同。

int func(int, int);int i = 0;func((i++, i++), 2); // 逗号表达式, 调用 func(1,2),i 的值变为 2func(i++, i++); // 未定义行为,“,”用于分隔不同的实参,两个 i++ 是无顺序的。

注意,被重载的逗号运算符实际将生成一个函数调用,对其操作数的求值遵循函数实参求值的规定,即不同操作数求值之间是无顺序。

11 序列初始化

C++11新引入的一种初始化语法。

在序列初始化中,{} 中可以存在多个初始化参数的求值。每个初始化参数的值计算与副作用先于被逗号分隔的后一个初始化参数的值计算与副作用。

int i = 0;std::vector<int> vec{i++, i++}; // OK 第一个 i++ 先于 第二个 i++ 的求值, i 变化 2 , vec 为包含两个元素(0,1)的vector。

注意与逗号表达式不同,这里的顺序的强制的,即使这个初始化引起了一个函数调用,而列表的每一个值作为函数的参数,这些求值之间的顺序依然会被保持。

3 0