C++中的操作符
本想博客以每周一篇的速度更新,却未曾料到最近几周忙到了没有时间坐下来写点东西的程度。
而这一篇,也因为写得较为匆忙,望您指出疏漏之处。
至于本文参考,可能部分来自于EC中的某个条款,并适当地加以补充。
在C++中,为基本类型定义操作符是最常见的任务。例如为一个自定义类型提供比较操作符,以允许其作为STL容器set的元素类型。在本文中,我们不讨论定义操作符的各种语法,而是简单介绍定义操作符过程中需要注意的一些问题。
首先需要明确的就是操作符的好处。相较于成员函数,操作符拥有更强的语义特征:一般情况下,一个操作符常常具有固定的意义。根据该固定意义为类型定义操作符可以使操作逻辑更为清晰。举例来说,表示4*4矩阵的Matrix44类可以通过定义乘法操作符以完成对矩阵乘法的支持,而不是通过Multiply()函数。另外,操作符的输入较成员函数更为方便,也使得代码更为简洁。
众多成熟类库都非常重视操作符的使用,如Boost或STL。这种重视同时体现在类库内建类型和对用户自定义类型的要求这方面。大家最为熟悉的智能指针以及STL容器就是非常典型的例子:智能指针重写了常用的指针操作符->及*,以使对它们的使用更像一个原生指针;而各个容器则常常需要容器所承载的类型提供特定的操作符,如set容器要求其所承载的数据类型提供比较操作符。这样模板等技术可以在特定操作符具有固定意义的假设下更好地操作各个类型,显著地增强模板代码的通用性。
同时,自定义操作符也不应被乱用。软件开发人员应当保证对操作符的重载保持其原有语意,否则对操作符的重载便失去了其积极意义,反而会导致误用。
在确保存在于某个自定义类型上的特定操作符拥有积极意义后,软件开发人员就可以开始着手实现该操作符。在实现之前,软件开发人员应考虑以下一些问题:操作符是否应作为类型的成员函数?如果不是,全局操作符应定义在哪里?是否需要使用explicit防止隐式调用?这些都是影响自定义操作符接口的一些因素,并在发生更改时可能影响用户代码。
首先讨论操作符是否应作为类型的成员函数。一旦一个操作符被定义为类型的成员函数,那么它将只能成为操作符的左值。此时软件开发人员就需要考虑将实例置于操作符左边是否恰当。判断是否恰当的一个准则就是操作符的使用是否遵循了一定的习惯,而这些习惯的形成则常常与操作符的结合顺序以及操作符的串联使用有关。如在类型X实现了操作符<<的情况下,软件开发人员就可以按照下列方式使用操作符:X << cout。而<<操作符是一个左结合操作符,那么X1 << X2 << cout将首先计算X1与X2的<<运算,而不是预期的X2 << cout。因此,<<操作符常需要被实现为非成员函数。
如果一个操作符需要对应的自定义类型作为操作符的右值,那么软件开发人员需要定义一个全局操作符,并在需要的情况下将其定义为类型的友元。该全局操作符的第二个参数需要是使用该操作符的自定义类型。
有时,操作符需要直接更改原数据,以提供更高的操作符执行性能。这种操作符常常作为类型的成员,并返回引用。最常见的例子就是+=等众多操作自身的操作符。
总结起来,软件开发人员通常可以使用以下规律判断一个操作符是否应实现为成员操作符:
1) 一元操作符应为成员。
2) =、()、[]和->都应实现为成员。
3) 由于需要更改内部状态,因此拥有赋值功能的各个操作符(+=、<<=、|=等)都必须是成员操作符。
4) 其它所有二元操作符都应实现为非成员操作符。
其中有几个特殊的操作符:.、?:、::以及*都不能重载。
如果决定将一个操作符定义为非成员函数,那么软件开发人员需要考虑该操作符定义所在的范围。一般情况下,该操作符的定义应当与该自定义类型处于同一个命名空间内。只有在这种情况下,不同命名空间的代码才能在ADL的辅助下正确查找到该操作符。
另一个需要讨论的问题则是操作符是否可以被隐式调用?需要考虑这个问题的常常是类型转换操作符。在操作符没有被explicit修饰的情况下,编译器可能在多种情况下自作主张地调用类型转化操作符,如在匹配函数调用的参数时。通过在操作符前使用explicit关键字,软件开发人员可以禁止编译器对类型转化操作符的调用。需要读者注意的是,在操作符前使用explicit关键字这种用法需要启用Visual Studio所提供的CLR扩展支持。
需要提到的是,类型转换操作符是操作符重载中的一个较为特殊的情况:其与构造函数和析构函数一样,遵守无返回类型的规定。
另一个与类型转换操作符有关的比较隐蔽的问题则是编译时其是否可以被隐式调用。假设类型Test定义了一个转化为TestClass的类型转换操作符,并且类型TestClass按照成员的方式定义了一个以int作为右值的加法操作符。那么对于Test类型的变量test,表达式test + 4将导致一个编辑错误。而如果软件开发人员将该操作符实现为全局操作符,那么编译器将顺利地完成编译。这是因为编译器在处理这两种情况时使用的是不同的机制:函数调用重载以及参数匹配。简单地说,编译器并不支持成员操作符查找时的隐式类型转化,而在使用全局操作符的时候,编译器将自动为参数查找转换操作符完成转换。
一旦确定了操作符的签名,软件开发人员就可以根据操作符所需要执行的实际功能完成对操作符逻辑的实现。在实现中常需要考虑的是操作符的性能:如果操作符被非常频繁地调用,那么该操作符的执行性能将是一个需要考虑的重要因素。一般来说,软件开发人员需要考虑的性能优化方式主要有内联以及NRV优化两种。内联一般适合具有较简单执行逻辑的操作符实现,而NRV优化则较为适合类型实例消耗较大的情况。
转载请注明原文地址:http://www.cnblogs.com/loveis715/archive/2012/01/08/2316688.html
商业转载请事先与我联系:silverfox715@sina.com