[转]http://www.cppblog.com/tiandejian/archive/2007/07/05/ec_24.html
我在这本书的序言中曾特别提到过,让类支持隐式类型转换在一般情况下都不会是一个好主意。当然,这一准则还是存在一些例外的,其中最普通的一个就是数值类型。举例说,如果你正在设计一个表示有理数的类,提供从整数向有理数的转换也不是毫无道理的。很显然,这样做与 C++ 内建的从 int 向 double 的转换 一样符合常理(甚至比 C++ 内建的从 double 向 int 的转换 要合理得多)。这是千真万确的,你可能以这样的方式开始编写你的 Rational (有理数) 类:
class Rational {
public:
Rational(int numerator = 0, int denominator = 1);
// 构造函数是有意写成非显性的
// 从而可以提供 int 到 Rational 的隐性转换
int numerator() const; // 用于访问分子和分母的函数
int denominator() const; // 参见第 22 条
private:
...
};
此时你很了解这个类应该支持诸如加法、乘法等算术操作,但是你并不能确定这些操作是应该通过成员函数实现,或者(如果可能的话)以非成员函数(友元)的形式实现。在你举棋不定的时候,你的本能会告诉你你应该尽量做到面向对象。你知道这一点,于是会说,有理数的乘法操作 与 Rational 类相关,因此很自然地,有理数的 operator* 就应该实现为 Rational 类内部的成员。与直觉恰恰相反的是,将函数放在相关的类中在有些时候恰恰是违背面向对象原则的(第 23 条中讨论过),我们暂时不考虑这一问题,考察一下用 operator* 作为 Rational 的一个成员函数:
class Rational {
public:
...
const Rational operator*(const Rational& rhs) const;
};
(如果你不太了解为什么以这种方式定义函数:返回一个 const 值而不是引用,使用“ const 引用”类型的参数。请参见第 3 、 20 、 21 条)
这种设计方案会使乘法操作非常简便:
Rational oneEighth(1, 8);
Rational oneHalf(1, 2);
Rational result = oneHalf * oneEighth; // 工作正常
result = result * oneEighth; // 工作正常
但是你不能满足于现状。你可能期望 Rational 支持混合模式操作,也就是说 Rational 应该可以与其它类型值(比如 int )相乘。毕竟说,两数相乘的操作再自然不过了,即使这两个数的类型不一致。
然而,当你尝试进行混合模式算术时,你会发现它仅仅在一半的时间内正常工作:
result = oneHalf * 2; // 工作正常
result = 2 * oneHalf; // 出错!
这是一个不好的兆头。你是否记得乘法交换率呢?
如果你将上述后两个示例重写为它们等价的函数形式,代码中的问题就会浮出水面:
result = oneHalf.operator*(2); // 工作正常
result = 2.operator*(oneHalf); // 出错!
oneHalf 对象是一个类的实例,这个类中包含 operator* ,于是编译器就会调用这个函数。然而整数 2 没有相关的类,因此就没有相关的 operator* 成员函数。编译器仍然会去寻找非成员函数 operator* (应该存在于名字空间域或者整体域),这些 operator* 应该可以这样调用:
result = operator*(2, oneHalf); // error!
但是在本示例中,没有任何非成员 的 operator* 能接收一个 int 和一个 Rational ,因此搜寻工作自然会失败。
请再次关注一下调用成功的示例。你可以看到它的第二个实在参数是整数 2 ,而 Rational::operator* 本身只将 Rational 作为它的型参。这里发生了什么呢? 2 为什么仅在一种情况下正常运行,而另一种又不可以了呢?
这里发生的事情是:隐式类型转换。编译器知道你正在传入一个 int ,而函数所需要的参数却是 Rational ,但是编译器同时也知道它可以通过使用你所提供的 int 值作为参数,调用 Rational 的构造函数,从而“变出”一个合适的 Rational 来。也就是说,编译器在处理上述代码时,会以近似于下面的形式进行:
const Rational temp(2); // 以 2 为参数,创建一个
// 临时的 Rational 对象
result = oneHalf * temp; // 与 oneHalf.operator*(temp) 等价
当然,编译器这样做仅仅是因为有一个非显性的构造函数为其助一臂之力。如果 Rational 的构造函数是 explicit 的,那么下面的语句都是通不过编译的:
result = oneHalf * 2; // 出错 ! ( 存在 explicit 的构造函数 )
// 无法将 2 转型为 Rational
result = 2 * oneHalf; // 同样的错误,同样的问题
看上去似乎仅在这些参数存在于参数表中的时候,它们才有资格进行隐式类型转换。与成员函数所调用的对象(也就是 this 所指向的对象)相关的隐式参数永远也没有资格进行隐式转换。这就是为什么第一次调用能够通过编译,而第二次不行。第一种情况涉及到参数表中所列的一个参数,而第二种没有。
但是此时你仍期望支持混合模式算术,同时在此时工作方案也水落石出了:将 operator* 声明为非成员函数,这样就可以允许编译器对所有参数进行隐式类型转换:
class Rational {
... // 不包含任何 operator*
};
const Rational operator*(const Rational& lhs, const Rational& rhs)
// 将 operator* 声明为非成员函数
{
return Rational(lhs.numerator() * rhs.numerator(),
lhs.denominator() * rhs.denominator());
}
Rational oneFourth(1, 4);
Rational result;
result = oneFourth * 2; // 工作正常
result = 2 * oneFourth; // 太棒了!这样也可以了。
故事终于有了一个完美的结局,但是还为人们留下了一处悬念。 operator* 是否应该做为 Rational 类的一个友元呢?
在这种情况下,答案是:不行。因为 operator* 完全可以通过 Rational 的公用接口来实现。上面的代码交待了如何做这件事情。我们可以从中观察总结出一条重要结论,那就是:与成员函数相反的是非成员函数,而不是友元函数。有太多的 C++ 程序员自认为,如果一个函数与一个类相关,那么就不应该将其实现为成员(比如说,所有参数都需要进行类型转换),而是应该实现为一个友元。这个实例表明这样的推理是存在漏洞的。要尽量避免使用友元,因为,与现实生活中的情况类似,朋友为我们带来的麻烦往往要比好处多得多。当然就像歌里唱的:“朋友多了路好走”,但是这并不意味着一个函数不应该作为成员时,就必须成为一个友元。
本条款中包含着真理,仅仅包含真理,而又不是真理的全部。当你从面向对象的 C++ 过渡至模板 C++ 时(参见第 1 条),你会将 Rational 实现为模板类而不是普通的类,此时就需要考虑新的问题了,也有了新的解决办法,一些设计实现的方法是不可思议的。这些问题、解决方案、具体实现是第 46 条讨论的主题。
铭记在心
l 如果你需要对一个函数的所有参数进行类型转换(包括 this 指针所指向的对象),那么它必须是一个非成员函数。