Effective C++ 条款 24、46

条款二十四: 若所有参数皆需类型转换,请为此采用non-member函数

class Rational
{
private:
    int numerator;
    int denominator;
public:
    Rational(int n = 0, int d = 1): numerator(n), denominator(d){assert(denominator != 0);}
    int GetNumerator() const{return numerator;}
    int GetDenominator() const {return denominator;}
    const Rational operator* (const Rational& r)
    {
        return Rational(numerator * r.numerator, denominator * r.denominator);
    }
};

有理数的私有成员是分子numerator和分母denominator,定义了构造函数,注意这个构造函数前面没有加explicit关键字,这意味着允许隐式转换(即标题所说的类型转换)。GetNumerator()和GetDenominator()是用来获取私有成员的值的,下面是我们要重点讲述的乘法运算符的重载。

我们的问题是,用什么方式实现乘法运算符的重载呢?我记得在我初学C++时,书上介绍了两种方法,一种是采用类内成员函数(也就是上面这段程序的写法),另一种是采用友元函数,像这样:

class Rational
{
private:
    int numerator;
    int denominator;
public:
    Rational(int n = 0, int d = 1): numerator(n), denominator(d){assert(denominator != 0);}
    int GetNumerator() const{return numerator;}
    int GetDenominator() const {return denominator;}
    friend const Rational operator* (const Rational& r1, const Rational& r2);
};
const Rational operator* (const Rational& r1, const Rational& r2)
{
    return Rational(r1.numerator * r2.numerator, r1.denominator * r2.denominator);
}

就这两种方法而言,其实友元函数的实现方式更优,为什么这样说?因为它们对封装性的冲击是相当的,但友元函数支持下面的代码:

int main()
{
    Rational r1(3, 5);
    Rational r2(2, 1);
    r2 = r1 * 2;
    r2 = 2 * r1;
    return 0;
}

成员函数实现却在r2 = 2 * r1上通不过编译。将这句话还原成运算符函数的形式,就能看到原因了:

r2 = 2.opeator* (r1);
哈哈,这样就知道为什么编译器不允许了,2必须首先转成Rational对象,才能使用它自身的operator*,但编译器不允许类型转换成this(隐含的自身指针)的,所以报错。

但对于友元函数实现,我们也将之还原成运算符函数的形式:

r2 = operator *(2, r1);
第一个参数会发生隐式转换(因为构造函数前面没有explicit),不会同时进行向this的转换,所以编译器是放行的。

这里插一句,隐式转换的发生过程就像这样:

1 const Rational temp(2);
2 r2 = operator*(temp, r1);
综上,成员函数实现不能支持含隐式转换的交换率,而友元函数实现却可以,故我们认为友元函数实现要优于成员函数实现(程序不支持乘法交换率是难以接受的)。

解决了这个问题之后,再来思考一下,能否对封装性进行改进,因为条款二十三说了,宁以non-member,non-friend函数去替换member函数,其实对友元函数形式稍加修改,去掉friend,改用public成员变量来访问函数内的私有成员,就可以实现好的封装,像这样:

const Rational operator* (const Rational& r1, const Rational& r2)
 {
     return Rational(r1.GetNumerator() * r2.GetNumerator(), r1.GetDenominator() * r2.GetDenominator());
 }

在这个函数里面,是不能直接访问到类的私有成员的,因而保证了好的封装性。

最后总结一下:

如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member,且为了封装,最好是个non-friend。

条款四十六:需要类型转换时请为模板定义非成员函数

这个条款可以看成是条款24的续集,我们先简单回顾一下条款24,它说了为什么类似于operator *这样的重载运算符要定义成非成员函数(是为了保证混合乘法2*SomeRational或者SomeRational*2都可以通过编译,2不能同时进行隐式类型转换成某个Rational,再作this用)。

所以我们一般将之定义成友元函数,像下面这样:

class Rational
{
private:
    int numerator;
    int denominator;
public:
    Rational(int n = 0, int d = 1): numerator(n), denominator(d){assert(denominator != 0);}
    int GetNumerator() const{return numerator;}
    int GetDenominator() const {return denominator;}
    friend const Rational operator* (const Rational& r1, const Rational& r2);
};
const Rational operator* (const Rational& r1, const Rational& r2)
{
    return Rational(r1.numerator * r2.numerator, r1.denominator * r2.denominator);
}

现在我们来引入模板,可以像下面这样写,注意这里的operator*是一个独立的模板函数:

template <class T>
class Rational
{
private:
    T Numerator;
    T Denominator;

public:
    Rational(const T& Num = 0, const T& Den = 1) : Numerator(Num), Denominator(Den){}
    const T GetNumerator() const
    {
        return Numerator;
    }

    const T GetDenominator() const
    {
        return Denominator;
    }

    string ToString() const
    {
        stringstream ss;
        ss << Numerator << "/" << Denominator;
        return ss.str();
    }
};

template <class T>
const Rational<T> operator* (const Rational<T>& a, const Rational<T>& b)
{
    return Rational<T>(a.GetNumerator() * b.GetNumerator(), 
        a.GetDenominator() * b.GetDenominator() );
}

但下面main函数的两行却都不能通过编译:

int main()
{
    Rational<int> a(3, 5);
    Rational<int> c = a * 2; // 不能通过编译!
    c = 2 * a;               // 不能通过编译!
    cout << c.ToString() << endl;
}

原因是编译器推导T出现了困难,a * 2在编译器看来,可以由a是Rational将T推导成int,但是2是什么,理想情况下编译器会尝试将它先转换成一个Rational,并将T推导成int,但事实上编译器在“T推导过程中从不将隐式类型转换函数纳入考虑”。所以无论是a * 2还是2 * a都是不能通过编译的,一句话,隐式转换+推导T不能被同时被编译器接受。

解决问题的思路便接着产生,编译器既然不能同时接受这两个过程,就让它们事先满足好一个条件,再由编译器执行另一个过程好了。

如果把这个operator*放在template class里面,也就是先在生成模板类的那一步就定下T,这样编译器只要执行隐式转换这一步就可以了。

因此我们可以这样来改:

template <class T>
class Rational
{
    …
    friend Rational operator* (const Rational& a, const Rational& b);
};

template <class T>
const Rational<T> operator* (const Rational<T>& a, const Rational<T>& b)
{
    // 这里友元函数的声明并不是用来访问类的私有成员的,而是用来进行事先类型推导的
    return Rational<T>(a.GetNumerator() * b.GetNumerator(), 
        a.GetDenominator() * b.GetDenominator() );
}

注意红色部分,我们添加了一个友元函数的声明,果然编译通过了,但链接时又报错了,原因是链接器找不到operator*的定义,这里又要说模板类中的一个特殊情况了,它不同与普通的类,模板类的友元函数只能在类中实现,所以要把函数体部分移至到类内,像下面这样:

template <class T>
class Rational
{
    …
    friend Rational operator* (const Rational& a, const Rational& b)
    {
        return Rational (a.GetNumerator() * b.GetNumerator(),
            a.GetDenominator() * b.GetDenominator());
    }
    …
}

这下编译和链接都没有问题了。这里还要说一下,就是移至类内后,T的标识符可以不写了,但如果非要写成下面这样,自然也是OK的。

1 friend Rational<T> operator* (const Rational<T>& a, const Rational<T>& b)
2 {
3     return Rational<T>(a.GetNumerator() * b.GetNumerator(),
4         a.GetDenominator() * b.GetDenominator());
5 }

operator*里面只有一句话,但如果friend函数里面的东西太多了,可以定义一个辅助方法,比如DoMultiply(),这个DoMultiply可以放在类外去实现,DoMultiply本身不支持混合乘法(2 * SomeRational或者SomeRational * 2),但由于在operator*里面已经进行了隐式类型转换,所以到DoMultiply这一级是没有问题的。

最后总结一下:

当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值