i = ++i + 1; // The behavior is unspecified
在介绍概念之前,我们先解释一下它的结果。这个表达式( expression )包含3个子表达式( subexpression ):
e1 = ++i
e2 = e1 + 1
i = e2
这三个子表达式都没有求值顺序点( sequence point ),而 ++ i 和 i = e3 都是有副作用( side effect )的表达式。假如最初 i = 0,前者令 i 变成1, 后者令 i变成2。由于没有求值顺序点,语言不保证这两个副作用的顺序。 所以最终 i 的值可能是1 ,也可能是2。
下面的术语以 ISO C99 和 C++03为准。译名为参考并附带原术语对照,如有解释不当或者错误望指正。
表达式有两种功能。每个表达式都产生一个值( value ),同时可能包含副作用( side effect ),比如:他可能修改某些值。
规则的核心在于 求值顺序点( sequence point ) [ C99 6.5 Expressions 条款2 ] [ C++03 5 Expressions 概述 条款4 ]。 这是一个结算点,语言要求这一侧的求值和副作用(除了临时对象的销毁以外)全部完成,才能进入下面的部分。 C/C++中大部分表达式都没有求值顺序点,只有下面五种表达式有:
1 函数。函数调用之前有一个求值顺序点。
2 && || 和 ?: 这三个包含逻辑的表达式。其左侧逻辑完成后有一个求值顺序点。
3 逗号表达式。逗号左侧有一个求值顺序点。
注意,他们都只有一个求值顺序点,2和3的右侧运算结束后并没有求值顺序点。
在两个求值顺序点之间,子表达式求值和副作用的顺序是不确定的。 可以看到,所有的赋值运算、自增自减都不包括在内,所以一旦某个表达式修改同一个变量超过一次,他的结果就是不确定的:
n = n++; // 两个副作用,顺序不确定
同时,几乎所有表达式,求值顺序都不确定。比如,下面的加法, f1 f2 f3的调用顺序是任意的:
n = f1() + f2() + f3(); // f1 f2 f3 调用顺序任意
而函数也只在实际调用前有一个求值顺序点。所以,常见于早期 C 语言教材的这类题目,是错题:
printf("%d",--a+b,--b+a); // --a + b 和 --b + a 这两个子表达式,求值顺序不确定
再看看C/C++标准中的其他例子:
[C99] 6.5.2.2 Function call
条款12 EXAMPLE 在下面的函数调用中:
(*pf[f1()]) ( f2(), f3() + f4() )
函数 f1 f2 f3 和f4 可能以任何顺序被调用。 但是,所有副作用都必须在那个 pf[ f1() ] 返回的函数指针产生的调用前完成。
[C++03] 5 Expressions 概论4
i = v[i++]; // the behavior is unspecified
i = 7, i++, i++; // i becomes 9 ( 译注: 赋值表达式比逗号表达式优先级高 )
i = ++i + 1; // the behavior is unspecified
i = i + 1; // the value of i is incremented
More Effective C++ 告诫我们, 千万不要重载 &&, || 和, 操作符[ MEC ,条款7 ]。为什么?
以逗号操作符为例,每个逗号左侧有一个求值顺序点。假如ar是一个普通的对象,下面的做法是无歧义的:
ar[ i ], ++i ;
但是,如果ar[ i ] 返回一个 class A 对象或引用,而它重载了 operator, 那么结果不妙了。那么,上面的语句实际上是一个函数调用:
ar[ i ].operator, ( ++i );
C/C++ 中,函数只在调用前有一个求值顺序点。所以 ar[i] 和 ++i 的求值、以及 ++i 副作用的顺序是任意的。这会引起混乱。
更可怕的是,重载 && 和 || 。 大家已经习惯了其速死算法: 如果左侧求值已经决定了最终结果,则右侧不会被求值。而且大家很依赖这个行为,比如是C风格字符串拷贝常常这样写:
while( p && *p )
*pd++ = *p++;
假如p 为 0, 那么 *p 的行为是未定义的,可能令程序崩溃。 而 && 的求值顺序避免了这一点。 但是,如果我们重载 && 就等于下面的做法:
exp1 .operator && ( exp2 )
现在不仅仅是求值混乱了。无论exp1是什么结果,exp2 必然会被求值。