Item 24: 当类型转换应该用于所有参数时,声明为非成员函数
当类型转换应该用于所有参数时,声明为非成员函数
最好不要提供隐式的类型转化, 但这条规则也存在特例,比如当我们需要创建数字类型的类时。正如 double 和int 能够自由地隐式转换一样, 我们的数字类型也希望能够做到这样方便的接口。
当然这一节讨论的问题不是是否应当提供隐式转换,而是如果运算符的所有“元”都需要隐式转换时,请重载该运算符为友元函数。
通过运算符重载来扩展用户定义类型时,运算符函数可以重载为成员函数,也可以作为友元函数。 但如果作为了成员函数,*this
将被作为多元操作符的第一元,这意味着第一元不是重载函数的参数,它不会执行类型转换。 仍然拿有理数类作为例子,下面的 Rational
类中,将运算符 *
重载为成员函数:
class Rational {
int n, d;
public:
Rational(int num = 0, int denom = 1) :n(num),d(denom){}
int numerator() const { return n; }
int denominator() const { return d; }
const Rational operator*(const Rational& rhs) const
{
return Rational(this->d*rhs.d, this->n*rhs.n);
}
};
我们看下面的运算符调用能否成功:
Rational oneHalf(1, 2);
Rational result = oneHalf * oneHalf; //@ OK
result = oneHalf * 2; //@ OK
result = 2 * oneHalf; //@ Error
第一个运算符的调用的成功是很显然的。我们看第二个调用:
当编译器遇到运算符 *
时,它会首先尝试调用:
result = oneHalf.operator*(2);
编译器发现该函数声明(它就是定义在Rational
类中的方法)存在, 于是对参数2
进行了隐式类型转换(long->Rational)。所以第二个调用相当于:
Rational tmp(2);
result = oneHalf.operator*(tmp);
将 Rational 的构造函数声明为 explicit 可以避免上述隐式转换,这样第二个调用也会失败。
对于第三个调用,编译器仍然首先尝试调用:
result = 2.operator*(oneHalf);
2 属于基本数据类型,并没有成员函数 operator*
。于是编译器再尝试调用非成员函数的运算符:
result = operator*(2, oneHalf);
再次失败,因为并不存在与 operator*(long, Rational)
类型兼容的函数声明,所以产生编译错误。 但如果我们提供这样一个非成员函数:
const Rational operator*(const Rational& lhs, const Rational& rhs);
这时候第一个参数也可以进行隐式转换。第三个调用(result = 2 * oneHalf
)便会成功,该表达式相当于:
Rational tmp(2);
result = operator*(tmp, oneHalf);
总结
- 如果你需要在一个函数的所有参数(包括被 this 指针所指向的那个)上使用类型转换,这个函数必须是一个非成员。