条款24:若所有参数皆需类型转换,请为此采用non-member函数
这个条款告诉了我们操作符重载被重载为成员函数和非成员函数的区别。作者想给我们提个醒,如果我们在使用操作符时希望操作符的任意操作数都可能发生隐式类型转换,那么应该把该操作符重载成非成员函数。
我们首先说明:如果一个操作符是成员函数,那么它的第一个操作数(即调用对象)不会发生隐式类型转换。
现在我们有一个Rational
类,并且它可以和int
隐式转换:
class Rational {
public:
Rational(int numerator = 0, int denominator = 1);
...
};
首先简单讲解一下当操作符被重载成员函数时,第一个操作数特殊的身份。操作符一旦被设计为成员函数,它在被使用时的特殊性就显现出来了——单从表达式你无法直接看出是类的哪个对象在调用这个操作符函数,不是吗?例如下方的有理数类重载的操作符”+”,当我们在调用Rational z = x + y;
时,调用操作符函数的对象并没有直接显示在代码中——这个操作符的this
指针指向x
还是y
呢?
class Rational {
public:
//...
const Rational operator+(const Rational& rhs) const;
pricate:
//...
}
作为成员函数的操作符的第一个隐形参数”this
指针”总是指向第一个操作数,所以上边的调用也可以写成Rational z = x.operator+(y);
,这就是操作符的更像函数的调用方法。那么,做为成员函数的操作符默认操作符的第一个操作数应当是正确的类对象——编译器正式根据第一个操作数的类型来确定被调用的操作符到底属于哪一个类的。因而第一个操作数是不会发生隐式类型转换的,第一个操作数是什么类型,它就调用那个类型对应的操作符。
我们举例说明:当Ratinoal
类的构造函数允许int
类型隐式转换为Rational
类型时,Rational z = x + 2;
是可以通过编译的,因为操作符是被Rational
类型的x
调用,同时将2
隐式转换为Ratinoal
类型,完成加法。但是Rational z = 2 + x;
却会引发编译器报错,因为由于操作符的第一个操作数不会发生隐式类型转换,所以加号“+”实际上调用的是2
—一个int
类型的操作符,因此编译器会试图将Rational
类型的x
转为int
,这样是行不通的。
因此在你编写诸如加减乘除之类的(但不限于这些)操作符、并假定允许每一个操作数都发生隐式类型转换时,请不要把操作符函数重载为成员函数。因为当第一个操作数不是正确类型时,可能会引发调用的失败。解决方案是,请将操作符声明为类外的非成员函数,你可以选择友元让操作符内的运算更便于进行,也可以为私有成员封装更多接口来保证操作符的实现,这都取决于你的选择。
const Rational operator+(const Rational& lhs, const Rational& rhs);
希望这一条款能解释清楚操作符在作为成员函数与非成员函数时的区别。此条款并没有明确说明该法则只适用于操作符,但是除了操作符外,我实在想不到更合理的用途了。
注:如果你想禁止隐式类型转换的发生,请把你每一个单参数构造函数后加上关键字explicit
。
条款 25:考虑写出一个不抛异常的swap函数
由于std::swap
函数在 C++11 后改为用std::move
实现,因此几乎已经没有性能的缺陷,也不再有像原书中所说的为自定义类型去自己实现的必要。不过原书中透露的思想还是值得一学的。
如果想为自定义类型实现自己的swap方法,可以考虑使用模板全特化,并且STL也是这种做法(public swap成员函数和std::swap特化版本):
class WidgetImpl{
public:
...
private:
int a, b, c;
std::vector<double> v;
...
}
class Widget {
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs){
...
*pImpl = *(rhs.pImpl);
...
}
void swap(Widget& other) {
using std::swap;
swap(pImpl, other.pImpl);
}
...
private:
WidgetImpl* pImpl;
};
namespace std {
template<>
void swap<Widget>(Widget& a, Widget& b) {
a.swap(b);
}
}
注意,由于外部函数并不能直接访问Widget
的private成员变量pImpl,因此我们先是在类中定义了一个 public 成员函数,再由std::swap
去调用这个成员函数。
若Widget
和WidgetImpl
是类模板,情况就没有这么简单了,因为 C++ 不支持函数模板偏特化,所以只能使用重载的方式:
template<typename T>
class WidgetImpl {...};
template<typename T>
class Widget {...};
namespace std {
template<typename T>
void swap(Widget<T>& a, Widget<T>& b) {
a.swap(b);
}
}
但很抱歉,这种做法是被 STL 禁止的,因为这是在试图向 STL 中添加新的内容(templates),所以我们只能退而求其次,在其它命名空间中定义新的swap函数:
namespace WidgetStuff {
...
template<typename T>
class Widget { ... };
...
template<typename T>
void swap(Widget<T>& a, Widget<T>& b) {
a.swap(b);
}
}
我们希望在对自定义对象进行操作时找到正确的swap函数重载版本,这时候如果再写成std::swap
,就会强制使用 STL 中的swap函数,无法满足我们的需求,因此需要改写成:
using std::swap;
swap(obj1, obj2);
这样,C++ 名称查找法则能保证我们优先使用的是自定义的swap函数而非 STL 中的swap函数。
C++ 名称查找法则:编译器会从使用名字的地方开始向上查找,由内向外查找各级作用域(命名空间)直到全局作用域(命名空间),找到同名的声明即停止,若最终没找到则报错。 函数匹配优先级:普通函数 > 特化函数 > 模板函数