之前一直认为C++中“&&”和“||”是不能被重载的。这种看法来源于几个事实。
首先看小段代码:
#include <iostream> int func(int i, int j) { return i + j; } int main() { int a = 1; int b = 0; b = func(a*=2, a+=1); std::cout << b << std::endl; return 0; }
这段代码会打印结果是不确定的。用一个确切的编译器编译后执行,会得到一个确切的结果。但是如果换一个编译器,结果可能又会不一样。为什么?
这里涉及到一个序列点(sequence point)的概念。序列点可以认为是表达式求值的一个点,在这个点执行的时候所有带有副作用(side effects)的计算必须已经结束,所有下一个序列点带有副作用的计算还没开始。关于序列点的完整信息可以参考:http://en.wikipedia.org/wiki/Sequence_point。
函数调用在进入函数体之前存在一个序列点,它规定了在这个点之前函数的参数必须已经全部求值,但没有规定参数的求值顺序。
之所以不限定参数求值顺序,个人的看法是为了给编译器厂商留下优化的空间,让编译器可以根据参数情况优化求值顺序。而不管编译器怎么优化,程序员只要保证各个参数的求值不会互相影响,就能得到一个确定的结果。
回到我们的代码func(a*=2,a+=1):
- 正如上面所说,函数调用的序列点只要求调用func之前a*=2和a+=1已经计算完成,但并没有规定它们的计算顺序。
- 对于默认的__cdecl函数调用,虽然参数入栈顺序是从右到左,但这并不影响各个参数自身的求值顺序。
- 因此a*=2和a+=1哪个先计算是不确定的,不能编译器会有不同的表现。
- 如果a*=2先算,那么结果是6,如果a+=1先算,那么结果是8。
程序的执行结果也证明了这一点:VS2010下运行结果是8,VC6.0下运行结果是6。
来看 “&&”和“||”,在这两个符合在求值时也有一个序列点规定:它们的左边必须先求值完成,才能根据情况继续求值。比如p!=0 && p->Done(),在执行p->Done()之前p!=0必须已经执行完。同时“&&”还保证如果左边求值为false,整个表达式就会为false,右边的表达式就不会被求值。而“||”则保证如果左边求值为true,整个表达式就会为true,右边的表达式就不会被求值。即在满足特定情况下,整个表达式的一部分会被忽略,这就好像电线短路了一样,所以这种情况也叫短路求值,很贴切。大量的代码的正确性都是依赖于“&&”、“||”的序列点规定和短路求值特征。
假设我们重载了“&&”,当我们写下p!=0 && p->Done()时,它实际上执行的是operator&&(p!=0, p->Done())。也就是重载之后“&&”就退化成函数调用了,这时候它遵循的是函数调用的序列点规则。也就是说这时候,p!=0和p->Done()不仅求值顺序变得不确定了,而且它们总是会被求值!那么到p真的为0时,p->Done()也会被执行,一般不会有什么好结果。
这大概就是我多年前的想法,当时就武断地认为“&&”、“||”不能被重载,并一直保留这个看法,直到昨天恰巧查找了C++中不可被重载的符号列表,发现里面并没有“&&”、“||”。同时自己写程序试了一下,也确定可以重载。而我也应该反思为什么这么久才发现这个问题。
附:C++中不可被重载的函数列表
Operator Name Syntax
Bind pointer to member by reference a.*b
Member a.b
Scope resolution a::b
Size of sizeof(a)
Ternary a ? b : c
Type identification typeid(a)
《The Design and Evolution of C++》里面有讲到“.”不可被重载的原因,讲得非常好,有兴趣的可以看看。