Declare non-member functions when type conversions should apply to all parameters
令 classes 支持隐式类型转换通常是个糟糕的注意。当然这条规定则有其例外,最常见的例外是在建立数值类型时。
假设你设计一个 class 用来表现有理数,允许整数 “ 隐式转换 ” 为有理数颇为合理。的确,它并不比 C++ 内置从 int 至double 的转换来得不合理,而还比 C++ 内置从 double 至 int 的转换来得合理些。
假设这样开始一个 Rational class:
class Rational{
public:
Ratonal(int numerator = 0, // 构造函数刻意不为 explicit,
int denominator = 1); // 允许 int-to-Rational 隐式转换
int numerator() const; // 分子(numerator)和分母(denominator)
int denominator() const; // 的访问函数(accessors)
private:
...
};
你想要支持加减乘除运算等等,但你不确定是否该由 member 函数、non-member 函数,或者可能的话由 non-member friend 函数来实现它们。你的直觉可能会告诉你把它放在 class 内实现。
不管怎样,现将 operator* 写成 Rational 的成员函数:
class Rational{
public:
...
const Rational operator* (const Ratiaonal& r) const;
};
(tip: 如果你不确定为什么这个函数被声明为此种形式,也就是为什么它返回一个 const by-value 结果却接受一个 reference-to-const 实参,请参考条款 3、20和21。)
这个设计使你能够将两个有理数以最轻松自在的方式相乘:
Rational oneEighth(1, 8);
Rational oneHalf(1, 2);
Rational result = oneEighth * oneHalf; // 很好
result = result * oneHalf; // 很好
但你不满足,你希望它支持混合运算,也就是拿 Rationals 和…例如 ints 相乘。毕竟两数相乘更令人自然——即便是两个不同类型的数值。
然而当你尝试混合式算术,你发现只有一半行得通,令人难过:
result = oneHalf * 3; // 很好
result = 2 * oneHalf; // 错误!
这不是个好兆头。乘法一个满足交换律,不是吗?
当你以对应的函数形式重写上述两个例子,问题就一目了然了:
result = oneHalf.operator*(2); // 很好
result = 2.operator*(oneHalf); // 错误!
是的,oneHalf 是一个内含 operator* 函数的 class 对象,所以编译器 调用该函数,然而整数 2 并没有相应的 class,也就没有 operator* 成员函数。所以编译器会尝试寻找可被以下这般调用的 non-member operator* (也就是在命名空间内或在 global 作用域内):
result = operator* (2,oneHalf);
但本例中并不存在这样一个接受 int 和 Rational 作为参数的 non-member operator*,因此会查找失败。
再来看看先前成功的那个调用。注意其第二个参数是 2,但 Rational::operator* 需要的实参却是个 Rational 对象。这究竟怎么回事呢?
因为这里发生了所谓的隐式类型转换。编译器知道你正在传递一个 int,而函数需要的是 Rational,但它也知道只要调用 Rational 构造函数并赋予你所提供的 int,就可以变出一个适当的 Rational 来。于是它就这样做了。换句话说此一调用有点像如下所示:
const Rational temp(2); // 根据 2 建立一个暂时性的 Rational 对象。
result = oneHalf * temp; // 等同于 oneHalf.operator*(temp);
当然,只因为这是 non-explicit 构造函数,编译器才会这样做。如果 Rational 构造函数是 explicit,以下语句均不能通过编译:
result = oneHalf * 2; // 错误
result = 2 * oneHalf; // 错误
这就很难让 Rational class 支持混合式算术运算了,不过至少上述两个句子的行为从此一致。
然而你的目标不止在一致性,也要支持混合式算术运算,也就是希望有个设计能让以上语句均通过编译。
解决办法:
让 operator* 成为一个 non-member 函数,也就是允许编译器在每一个实参上执行隐式类型转换:
class Rational{ ... }; // 不含 operator*
const Rational operator*(const Rational& l,
const Rational& r) // 现在成了 non-member 函数
{
return Rational( l.numerator() * r.numerator(), l.denominator() * r.denominator() );
}
Rational oneFourth(1, 4);
Rational result;
result = oneFourth * 2; // 没问题
result = 2 * oneFourth; // 万岁,通过编译啦!
这当然是个快乐的结局,不过还有一点必须操心:operator* 是否应该成为 Rational class 的一个 friend 函数呢?
答案是否定的,因为 operator* 可以完全藉由 Rational 的 public 接口完成任务,上面的代码已经表明此种做法。
请记住:
- 如果你需要为某个函数的所有参数(包括被 this 指针所指的那个隐喻参数)进行类型转化,那么这个函数必须为 non-member 函数。