Item 46:需要类型转换时,应当在类模板中定义非成员函数

Item 46: Define non-member functions inside templates when type conversions are desired.

Item 24中提到,如果所有参数都需要隐式类型转换,该函数应当声明为非成员函数。 Item 24是以Rational和operator*为例子展开的,本文把这个观点推广到类模板和函数模板。 但是在类模板中,需要所有参数隐式转换的函数应当声明为友元并定义在类模板中。

模板化的Rational


既然是Item 24的推广,那么我们先把Item24中的Rational和operator*模板化。得到如下代码:

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){}

Item 20解释了为什么Rational的参数是常量引用;Item 28解释了为什么numerator()返回的是值而不是引用;Item 3解释了为什么numerator()返回的是const。

模板参数推导出错


上述代码是Item24直接模板化的结果,看起来很完美但它是有问题的。比如我们有如下的调用:

Rational<int> oneHalf(1, 2);            // OK
Rational<int> result = oneHalf * 2;     // Error!

为什么第二条会出错呢?因为编译器无法推导出合适的模板参数来实例化Rational。 模板参数的推导包括两部分:

  • 根据onHalf,它的类型是Rational,很容易知道接受oneHalf的operator*中模板参数T应该是int;
  • 根据2的模板参数推导却不那么顺利,编译器不知道如何将实例化operator*才能使得它接受一个int类型的2。

可能你会希望编译器将2的类型推导为Rational,再进行隐式转换。但在编译器中模板推导和函数调用是两个过程: 隐式类型转换发生在函数调用时,而在函数调用之前编译器需要实例化一个函数。而在模板实例化的过程中,编译器无从推导T的类型。

声明为友元函数

为了让编译器知道T是什么,我们可以在类模板中通过friend声明来引用一个外部函数。

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){}
在Rational<T>中声明的friend没有添加模板参数T,这是一个简便写法,它完全等价于: friend const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs);。

因为类模板实例化后,T总是已知的,因而那个friend函数的签名会被Rational模板类声明。 这样,result = oneHalf * 2便可以编译通过了,但链接会出错。 虽然在类中声明了friend operator*,然而编译器却不会实例化该声明对应的函数。 因为函数是我们自己声明的,那么编译器认为我们有义务自己去定义那个函数。

在类中给出定义


那我们就在声明operator*时直接给出定义:

template<typename T>
class Rational {
public:
    friend const Rational operator*(const Rational& lhs, const Rational& rhs){
        return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
    }
};

这样混合模式的调用result = oneHalf * 2终于可以编译、链接并且运行了。到这里想必问题已经很清楚了:

  1. 为了对所有参数支持隐式类型转换,operator*需要声明为非成员函数;
  2. 为了让编译器推导出模板参数,operator*需要在类中声明;
  3. 在类中声明非成员函数的唯一办法便是声明为friend;
  4. 声明的函数的同时我们有义务给出函数定义,所以在函数定义也应当放在friend声明中。

调用辅助函数


虽然operator* 可以成功运行了,但定义在类定义中的函数是inline函数,见Item 30。 如果operator* 函数体变得很大,那么inline函数就不再合适了,这时我们可以让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 Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs){
        return doMultiply(lhs, rhs);
    }
};

doMultiply仍然是不支持混合模式调用的,然而doMultiply只会被operator* 调用。 operator*将会完成混合模式的兼容,然后用统一的Rational< T>类型参数来调用doMultiply。

转载地址:http://harttle.land/2015/09/14/effective-cpp-46.html
感谢作者 Harttle

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值