条款24:若所有参数皆需类型转换,请为此采用non-member函数
Declare non-member functions when type conversions should apply to all parameters.
本章分为两部分。
首先,有以下这条规则:
- 令class支持隐式类型转换通常是一个糟糕的选择。
但是,这条规则也有例外,最常见的就是建立数值类型时。
假设对于一个class用来表示有理数,因此允许整数“隐式转换”为有理数看起来其实是挺合理的:
class Rational {
public:
Rational(int numerator = 0, //构造函数刻意不是explicit
int denominator = 1); //允许int-to-rational进行隐式转换
int numerator() const; //分子的访问函数
int denominator() const; //分母的访问函数
private:
...
};
接着,对于算数运算的实现,到底应该使用member函数、non-member函数,还是non-member friend函数来实现?
首先,对于operator* 的实现,虽然在条款23中指出,将函数放进相关class内又是会与面向对象守则发生矛盾,但先暂时不考虑,先看一下将operator* 写成Rational 成员函数的写法:
class Rational {
public:
...
const Rational operator* (const Rational& rhs) const;
};
这种设计,可以以很方便的方式实现相乘:
Rational oneEight(1, 8);
Rational oneHalf(1, 2);
Rational result = oneHalf * oneEight; //成功!
result = result * oneEight; //成功!
暂时看上去,是可行的。但是如果我们此时用两个不同类型的数据进行相乘——比如,一个Rational和int相乘,就可能会出现问题:
result = oneHalf * 2; //成功!
result = 2 * oneHalf; //错误!
为什么会出现这样的错误??当我们以对应的函数形式重写上述两行代码:
result = oneHalf.operator*(2); //成功!
result = 2.operator*(oneHalf); //错误!
因此,错误就显而易见了:
- oneHalf是一个内含operator* 函数的class的对象,因此没有问题。
- 整数2并没有对应的class,也就没有operator* 成员函数。
此时,编译器也会尝试在命名空间内或在global作用域内调用以下形式的non-member operator*:
result = operator*(2, oneHalf); //错误!
然而在此例中,并不存在这样一个接受int和Rational作为参数的non-member operator*, 因此会查找失败。
在这里,上面第一次有参数2,之所以成功,是因为这里发生了所谓的隐式类型转化(implicit type conversion)。
编译器知道此时确实是传递了一个int,而函数需要的却是Rational;但编译器同时也知道,只要它调用Rational构造函数并赋予传递来的int,就可以构造出适当的Rational出来。换句话说,此时的调用动作在有点像以下的形式:
const Rational temp(2); //根据2建立一个暂时性的Rational对象
result = oneHalf * temp; //等同于oneHalf.operator*(temp)
这也只是因为涉及到了non-explicit构造函数,编译器才会这样去实现。如果 Rational的构造函数是explicit,下面两条语句都是错误的:
result = oneHalf * 2; //错误!无法将2转换位Rational
result = 2 * result; //一样的错误!
此时我们可以看到,这就很难让Rational class支持混合式算数运算了。