关于C/C++中的side effect(负效应)和sequence point(序列点)
不知你在写code时是否遇到这样的问题?int i = 3; int x = (++i) + (++i) + (++i); 问x值为多少?进行各种理论分析,并在编译器上实践,然而可能发现最终的结果是不正确的,也是不稳定的,不同的编译器可能会产生不同的结果。这让人很头疼。结果到底是啥呢?对于此题的答案,一句话,Theresult is undefined! 详细解释待我慢慢说来。
大家知道,通常而言,我们写的计算机程序都是从上到下,从左到右依次执行。然而,我只是说通常,因为在编译的过程中,compiler并不仅仅是把source code翻译成binary code就算了,这个过程里面可能还会对代码进行优化,这种优化可能带来的结果是:代码或者表达式evaluation的顺序可能发生变化。这可是一个非常严重的问题,当某个表达式带有side-effect(比如改变了一个变量的值),那么它的执行顺序直接影响到了程序执行的结果。
为了保证程序执行具有确定性的结果,C++标准引入Sequence Point这个概念,按照ISO/IEC的定义:
At certain specified points in the execution sequence called sequence points. All side effects of previous evaluations shall be complete and no side effects of subsequent evaluations shall have taken place.
简而言之,Sequence Point就是这么一个位置,在它之前所有的side effect已经发生,在它之后的所有side effect仍未开始,而两个Sequence Point之间所有的表达式或者代码执行的顺序是未定义的!
而C++标准又进一步规定了Sequence Point出现的5种情况:
1、At the end of a full expression
在一个完整的表达式末尾是Sequence Point,所谓完整的表达式是指这个表达式不是另外一个表达式的一部分。所以如果有f(); g();这样两条语句,f()和g()是两个完整的表达式,f()的Side Effect必定在g()之前发生。
2、After the evaluation of all function arguments in a function call and before execution of any expressions in the function body
调用一个函数时,在所有准备工作做完之后、函数调用开始之前是Sequence Point。比如调用foo(f(), g())时,foo、f()、g()这三个表达式哪个先求值哪个后求值是Unspecified,但是必须都求值完了才能做最后的函数调用,所以f()和g()的Side Effect按什么顺序发生不一定,但必定在这些Side Effect全部作用完之后才开始调用foo函数。
3、After copying of a returned value and before execution of any expressions outside the function
函数即将返回时是Sequence Point,因为函数返回时必然会结束掉一个完整的表达式。
4、After evaluation of the first expression in a&&b, a||b, a?b:c, or a,b
条件运算符?:、逗号运算符、逻辑与&&、逻辑或||的第一个操作数求值之后是Sequence Point。如条件运算符和逗号运算符,条件运算符要根据表达式1的值是否为真决定下一步求表达式2还是表达式3的值,如果决定求表达式2的值,表达式3就不会被求值了,反之也一样,逗号运算符也是这样,表达式1求值结束才继续求表达式2的值。
5、After the initialization of each base and member in the constructor initialization list
在一个完整的声明末尾是Sequence Point,所谓完整的声明是指这个声明不是另外一个声明的一部分。比如声明int a[10], b[20];,在a[10]末尾是Sequence Point,在b[20]末尾也是。
经过以上说明,大家已有所了解,现在回到我们的题目:int x = (++i) + (++i) + (++i); 整个的语句里面,只有1个Sequence Point,也就是语句的结束点,对于右边表达式的计算顺序没有任何的规定,显然,各种编译器都可以按照他们觉得“舒服”的方式来进行计算,这样的代码,如果只要求在特定的平台或者编译器运行,那么带来的可能只是可读性差的问题,但如果考虑跨平台或者编译器的情况,那么就是完完全全的错误!
另外,需要特别注意的是,对于赋值号(assignment operator),C++也没有把它定义成Sequence Point,也就说这样的语句:buffer[i] = i++;同样是undefined的,因为,对于等号左右两边的表达式运算顺序,你并不能有任何的假定。