Effective C++条款46:模板与泛型编程之(需要类型转换时请为模板定义非成员函数)

一、非成员函数模板出错的例子

  • 我们在条款24说过,对于Rational类来说,为了让所有的乘法操作都能通过操作,我们将operator*()函数定义为非成员函数版本(详情可以回过头再去看)。但是对于模板来说,这些规则可能不成立

  • 例如下面我们把Rational和operator*()函数都定义为模板,代码如下:
//下面与条款24都相同,只是改为了模板
template<typename T>
class Rational {
public:
    Rational(const T& numerator = 0, const T& denominator = 1);
    const T numerator()const;
    const T denominator()const;
};

template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs)
{	
}
  • 现在我们书写下面的代码,不能编译通过。例如:
Rational<int> oneHalf(1, 2);

//在条款24中,Rational和operator*为非模板,此处代码可编译通过
//此处,Rational和operator*为模板,编译不通过
Rational<int> result = oneHalf * 2;

错误原因分析

  • 错误的原因在于:上面的“oneHalf*2”没有找到匹配的operator*()函数,从而导致出错
  • 原因:
    • oneHalf的类型为Rational<int>,因此其匹配到operator*()函数模板,并将operator*()函数模板的模板类型T实例化为int
    • 但是当运行到第二个参数的时候,2为int,operator*()函数模板无法根据int推断出T的类型
    • 因此最终编译器找不到合适的operator*()函数而导致出错
  • 在非模板的operator*()中,operator*()可以将2隐式转换为Rational对象(Rational提供了隐式转换,详情再去看条款24)。但是在模板中,这种隐式转换不被允许
  • template实参推导取到决定性的因素

二、将模板函数声明为friend

  • 在“一”中,我们使用的模板类和模板函数出错了,一种解决办法是将operator()*声明为Rational模板类的友元(friend)
  • 下面我们进过两次修改,最终实现错误的代码

第一次修改(编译通过,但是链接时出错)

  • 我们先将operator()*声明为Rational的友元。代码如下:
template<typename T>
class Rational {
public:
    //友元,函数声明
    friend const Rational operator*(const Rational& lhs, const Rational& rhs);
};

//函数定义
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs);


int main()
{
    Rational<int> oneHalf(1, 2);
    //编译通过,但是链接时出错
    Rational<int> result = oneHalf * 2;

    return 0;
}
  • 备注:上面friend生命中,我们没有在Rational后面添加<T>,这与Rational<T>是一样的,这两都可以
  • 编译通过的原因:
    • 当对象oneHalf被定义时,程序会生成一份Rational<int>类的定义,于是class Rational<int>被实例化出来
    • class Rational<int>被实例化之后,friend operator*()函数也被自动声明出来(备注:只是声明而没有定义)
    • friend operator*()函数声明出来之后,该函数为一个函数了,而不是一个模板了,因此我们可以将2传递给该函数(并且参数2可以隐式类型转换,将2转换为Rational)
    • 于是上面的代码就可以编译通过了,但是链接时出错(下面介绍)
  • 链接时出错的原因:
    • 因为在class Rational<int>被实例化出来的时候,friend只是将operator*()声明了,但是没有定义,因此程序报错

第二次修改(正确版本)

  • 上面的代码编译通过,但是链接通过,因为找不到operator*()的定义
  • 解决链接出错的方法是:在friend函数的声明同时,定义这个函数。代码如下:
template<typename T>
class Rational {
public:
    //声明的时候,同时实现该函数(备注,该函数虽然在类中定义,但不属于成员函数)
    friend const Rational operator*(const Rational& lhs, const Rational& rhs)
    {
        return Rational(lhs.numerator()*rhs.numerator(),
        lhs.denominator()*lhs.denominator());
    }
};
  • 此处friend的另一个好处:我们虽然将该函数声明为friend,但是却没有访问任何class的non-public成员,因此比较安全

三、令friend函数调用辅助函数(与inline相关)

  • 我们曾在条款30中说过,如果函数定义在class中都会成为inline,因此上面我们定义在class中的friend operator()*也会称为inline
  • 为了使inline带来的冲击最小化,我们可以:
    • 我们可以在operator()*中什么事情都不做,将所完成的事情移动到另一个辅助函数中
    • 这个辅助函数应该是一个template
  • 代码如下:

    • ​​​​​​​为了使operator*()在类中的inline最优化,我们定义了一个辅助函数,在其中完成operator*()原本要完成的功能

    • doMultiply()不支持混合式操作(例如将Rational对象和一个int相乘),但是该函数只由operator*()调用,而operator*()支持混合操作,因此功能完善

//声明
template<typename T> class Rational

//函数声明(定义在下)
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs);

template<typename T>
class Rational {
public:
    friend
        const Rational operator*(const Rational& lhs, const Rational& rhs)
    {
        //在其中调用doMultiply(),可使inline最优化
        return doMultiply(lhs, rhs);
    }
};

//完成原本operator*()要完成的功能
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs)
{
    return Rational<T>(lhs.numerator()*rhs.numerator(),
        lhs.denominator()*lhs.denominator());
}

​​​​​​四、总结

  • 当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”
上一篇:python-app自动化查找微信僵尸好友


下一篇:C++ 类中会用到的关键字之-friend(virtual、override、friend、default、delete、final)