一篇老文章,原本在网易博客的,结果博客关停了。
操作符重载涉及到一些类设计方面的东西,同时也有C++中名字搜索等。
下面是C++标准中说明的可以被重载的操作符:
new delete new[] delete[]
+ - * / % ˆ & | ∼
! = < > += -= *= /= %=
ˆ= &= |= << >> >>= <<= == !=
<= >= && || ++ -- , ->* -> ( ) [ ]
其中同时可用于一元和二元的操作符有 + - * &
不可被重载的操作符有 . .* :: ?:
上面的4个操作符不能被重载是因为其参数为名称,而非值。
同样不能被重载的还有sizeof和typeid,当然还有四个C++引入的强制转换符static_cast, const_cast, dynamic_cast, reinterpret_cast。
关于操作符的重载,我们按类型可以分为一元操作符和二元操作符。其中一元操作符(前缀或者后缀)既可以定义为无参数的非静态成员函数,也可以定义为取一个参数的非成员函数;解释的规则是:对于操作符@,@aa可以被解释为aa.operator()或者operator@(aa)。对于二元操作符既可以定义为取一个参数的非静态成员函数,也可以定义为取两个参数的非成员函数;解释规则是:对于操作符@,aa@bb可以解释为aa.operator@(bb)或者operator@(aa, bb)。不管是一元还是二元的操作符重载,如果用两种重载方式定义了操作符,那么按照重载解析规则来确定到底是要用哪一个,不过成员函数版的操作符重载并不比非成员函数的操作符重载优先级高。还有一点要注意,在非成员函数的定义中,必须至少有一个参数是用户自定义类型,因为这一点要保证不改变原有表达式的意义。
函数定义的形参和函数调用时的实参之间的匹配原则是:尽可能调用匹配的最好的那个函数。下面是具体的参数匹配原则:
[1] 准确匹配,也就是说无需任何转换,或者只需做普通转换(比如数组名到指针,函数名到函数指针,T到const T等)的匹配。
[2] 利用提升的匹配,就是包括整数提升(bool到int,char到int,short到int以及对应的无符号版本)以及float到double的提升。
[3] 利用标准转换(int到double, double到int,double到long double,Derived*到Base*,T*到void*,int到unsinged int)的匹配。
[4] 利用用户定义转换的匹配。
[5] 利用在函数声明中的省略号...的匹配
如果以上匹配得到的不止一个函数,那么将存在歧义,编译器不会通过的。
运算符重载函数也是位于某一个命名空间的,相同的名字在不同的命名空间中是不同的。这里就涉及到重载解析的时候关于命名空间对匹配的影响。并且如果没有下面这条规则,我们就无法实现输入输出操作符(<<, >>)的定义,因为这个符号操作很特别。
假如操作符@是二元操作符,x是类型X,y是类型Y,那么x@y将会按照如下的方式解析:
以上规则,如果有一条成功,就不会往后执行了。一元操作符的规则也类似。关于命名空间、名字查找和接口规则在[B.4]中的第5章中有精彩的描述,Hurb Sutter告诉我们有时候(很碰巧),实际被调用的函数并不是我们所想的那样。
对于那些可以被重载的操作符,我们最好不要重载&&, ||和逗号操作符(,),因为我们无法实现&&和||的短路求值规则,因为C++没有规定参数的求值顺序,以及无法实现逗号操作符(参考[B.5]中的条款7)。
为了减少重载函数的个数,一般地,我们可以用inexplicit的构造函数转换来实现将一种数据转换为自定义类型,当然这种隐式转换最多只能做一次;反过来,我们也可以将自定义类型转化为某一种类型,只要我们重新定义强制转换的符号即可,这个定义有点像构造函数,不能有返回值,但是和构造函数不同的是它可以return想要的值。当然了,大多数时候我们并不想让隐式转换发生,这时候我们应该用explicit来修饰单参数构造函数;如果我们想要自定义类型转为另一种类型的值,可以直接定义这种成员函数,比如说asString(),asInt()……
有时候我们为了定义一个操作,来让两种自定义的类型作为其参数,这时候我们可以使用友元函数。我们可以在两个类中将这个操作符的重载函数声明为友元函数,从而可以让任何一个都可以作为对象来调用其operator。
对于不同的操作符,我们是应该将其实现为成员函数,或者非成员函数,或者静态成员函数,或者友元函数,再或者某几种都可以,或者只能是某种。这里面就有一些情况需要一一说清楚,并且还有一些重载操作符方面的技巧和好的经验需要说明一下。
关于隐式转换有一点要提出,就是非const类型不能用于隐式转换:
double &d = 3.14; //error
const double& cd = 3.14; //ok
[B.2]的11.5.2中意思是,当我们在考虑重载操作符的时候,应该先考虑是否可把某操作设计为自由函数,再考虑是把该函数设计为成员函数还是友元函数。
基于前述内容,具体考虑如何设计重载操作符函数的方法是这样的:
很显然,重载的操作符最好要与内置类型保持一致。下面就讲一些常用的、有一些技巧的操作符重载:
不过,我有点奇怪为什么没有~=复合操作符,并且几乎没见过->*(数据成员指针访问符)的定义。
关于new/delete的重载参考我的另二篇文章[D.1],[D.2]。
最后,至于重载中的一些两可的重载函数可以根据自己的喜好,关于一些返回引用参数的重载,至于要不要用const修饰,也是看具体的应用场景。
参考资料
Doc
[D.1] C++中new与delete的定制
[D.2] placement new/delete释疑
Book
[B.1] Stanley B. Lippman,《C++ Primer 5th》中文版/英文版
[B.2] Bjarne Stroustrup,《The C++ Programming Language 4th Edition》
[B.3] C++标准文档,ISO/IEC 14882,13.5 Overloaded operators
[B.4] Herb Sutter,《Exceptional C++》
[B.5] Scott Meyers,《More Effective C++》