1.问题的引入
C++支持隐式类型转换,但通常情况下是不好的。然而,本这条规定也有例外。最常见的例外情况发生在建立数值类型时,假设你开始设计如下有理数类Rational:
class Rational { public: Rational(int numerator = 0, int denominator = 1); // 注意:此处的构造函数为隐式的,因为没有使用explicit关键字修饰 int numerator() const; int denominator() const; private: // ... };
你想支持加减乘除等算术运算,但你不确定是否该有成员函数、non-member函数、non-member non-friend函数来实现它们。直觉告诉我们,当我你犹豫不决时,请从面向对象的角度去思考。因为有理数相乘与Rational类有关,因此很自然地想应该在Rational类的内部为有理数实现重载*运算符函数opeator*()。下面,先考虑将operator*()写成Rational成员函数的写法:
class Rational { public: // ... // 成员函数 const Rational operator* (const Rational& rhs) const; // 注意:成员函数的形参中有一个默认参数this,指向的是调用该成员函数的对象! private: // ... };
假设你不明白为啥上面的成员函数被声明为此种形式,即它返回一个const by-value结果,但接收的参数是一个reference-to-const实参,请先学习前面的文章尽量以const、enum、inline替换#define、C++中多用引用传递方式替换值传递方式、C++中不要随便返回对象的引用。上面的成员函数能够让你将两个有理数以最普通的方式进行相乘。如下所示:
Rational oneEighth(1, 8); Rational oneHalf(1, 2); Rational result = oneHalf * oneEighth; result = result * oneEighth;
但是,你以为这样就结束啦?不不不,你还希望支持混合运算即有理数与int类型之间的相乘。但是,当你尝试混合运算的时候,不幸发生了,你发现只有一半行得通。如下所示:
Rational oneHalf(1, 2); Rational result = oneHalf * 2; // 正确 result = 2 * oneHalf; // 错误
出问题了哦,乘法不是应该满足交换律嘛。如果你对上面的代码段进行重写,如下所示:
Rational oneHalf(1, 2); Rational result = oneHalf.operator*(2); result = 2.operator*(oneHalf);
2.分析问题,追根溯源
正如你所思考的那样,oneHalf是一个内含operator*成员函数的类的对象,所以编译器调用该函数。但是,整数2并没有相应的类,也就没有operator*成员函数。编译器也会试图寻找(即在命名空间或global作用域内查找)可以被以下调这样调用的non-member函数operator*
result = operator*(2, oneHalf); // 错误
但是,本例中并不存在这样接收int和Rational作为参数的non-member函数operator*,因此查找失败。再回过头看看先前成功的那个调用,注意它的第二个参数是int类型2,但是Rational::operator*需要的实参可是Rational对象哦。这是咋回事呢?为啥2在这里就可以接收,而在另一个调用中就不可以呢?
原因:因为这里发生了所谓的隐式类型转换。编译器知道你正在传递一个int,而函数需要的却是Rational,但它也知道只要调用Rational构造函数并赋予你所提供的int,就可以变出一个适当的Rational,于是编译器就那样做了。换句话来说,调用动作在编译器眼中有点像下面的代码:
const Rational temp(2); // 根据整型2建立创建一个临时的 Rational对象 result = oneHalf * temp; // 等价于oneHalf.operator*(temp)
当然,只因为构造函数是隐式的,即non-explicit构造函数,编译器才会去这样做。如果Rational的构造函数是explicit,下面的代码没有一个是正确的。
result = oneHalf * 2; // 错误! (在explicit显式构造函数的情况下,无法将2转换为Rational) result = 2 * oneHalf; // 仍然错误!
如果你就是希望能希望支持有理数的混合算术运算,即希望有方法将以上的两个语句都可以通过编译。现在,我们继续探究为什么即使Rational构造函数不是显示explicit的,仍然只有一个可以通过编译呢?
result = oneHalf * 2; // 正确! (在non-explicit构造函数的情况下,可以将2转换为Rational) result = 2 * oneHalf; // 错误!
根本原因:只有当参数出现在构造函数的初始化列表中,这个参数才是隐式类型转换的合格参与者。这是因为成员函数本身就自带了一个隐藏参数this指针,它指向调用成员函数的那个对象。因此,第一次调用可以通过,第二次调用是失败的。因为第一次调用伴随一个放在构造函数初始化列表中的参数,第二次调用则不是。
3.柳暗花明又一村
最终解决方法:让operator*成为一个non-member函数,允许编译器在每个实参身上执行隐式类型转换。如下所示:
class Rational { public: Rational(int numerator = 0, int denominator = 1); // 注意,此处的构造函数为隐式的,因为没有使用explicit关键字修饰 int numerator() const; int denomiator() const; private: // ... }; // non-member函数 const Rational operator* (const Rational& lhs, const Rational& rhs){ return Rational(lhs.numerator() * rhs.numerator(), lhs.denomiator() * rhs.denomiator()); } int main(){ Rational oneFourth(1, 4); Rational result; result = oneFourth * 2; // result = 2 * oneFourth; 也通过了编译! return 0; }
此外,还有一点需要考虑:operator*是否应该成为Rational类的友元函数呢?答案是否定的。因为operator*可以完全通过Rational的public接口完成任务,上面的代码段就是这样做的。因此,可以得到一个结论:成员函数的对立面是non-member函数,而不是友元函数。现实中太多的程序员假设,如果一个与某类相关的函数不是一个它的成员函数,就应该是该类的友元函数。这实际上是错误的观点,上面的代码段就可以证明这个观点太过牵强。记住:无论何时如果你可以避免友元函数就去避免它,因为不像现实生活中“多个朋友多条路”。在C++程序中,友元函数带来的麻烦往往多于其产生的价值。
4.总结
(1) 如果你需要为某个函数的所有参数(包括this指针所指向的那个隐藏参数)进行类型转换,那么这个函数必须是non-member。