Item 24: Declare non-member functions when type conversions should apply to all parameters.

虽然Item 15:资源管理类需要提供对原始资源的访问中提到,最好不要提供隐式的类型转化。 但这条规则也存在特例,比如当我们需要创建数字类型的类时。正如doubleint能够自由地隐式转换一样, 我们的数字类型也希望能够做到这样方便的接口。 当然这一节讨论的问题不是是否应当提供隐式转换,而是如果运算符的所有“元”都需要隐式转换时,请重载该运算符为友元函数

通过运算符重载来扩展用户定义类型时,运算符函数可以重载为成员函数,也可以作为友元函数。 但如果作为了成员函数,*this将被作为多元操作符的第一元,这意味着第一元不是重载函数的参数,它不会执行类型转换。 仍然拿有理数类作为例子,下面的Rational类中,将运算符*重载为成员函数:

class Rational{
public: 
    Rational(int n = 0, int d = 1);
    int numerator() const;
    int denominator() const;
    const Rational operator*(const Rational& rhs) const;
...

我们看下面的运算符调用能否成功:

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);

只有当运算符的元出现在运算符函数的参数列表时,它才会被隐式类型转换。所以当我们需要运算符的所有“元”都可以被隐式转换时, 应当将运算符声明为非成员函数。 在JavaScript或者C#中,这个规则是不需要的,因为编译器/解释器在这里做了更多的工作。比如JavaScript中2.toFixed(3) 会被解释为Number(2).toFixed(3),该表达式的值为"2.000"`。

本文采用 知识共享署名 4.0 国际许可协议(CC-BY 4.0)进行许可,转载注明来源即可: https://harttle.land/2015/08/22/effective-cpp-24.html。如有疏漏、谬误、侵权请通过评论或 邮件 指出。