条款46:需要类型转换时请为模板定义非成员函数
Define non-member functions inside templates when type conversion are desired.
还记得在条款24中,我们提到只有non-member函数才有能力'在所有实参身上实施隐式类型转换'.今天我们讨论的
东西与该条款相关,如果现在的你恰好忘记了前面这条款的内容,那么我建议你还是先把那条款在消化一下,再开始
这一款吧,呵呵,'磨刀不误砍柴工'嘛!废话我就不多说了,我们进入正题.
'只有non-member函数才有能力在所有实参身上实施隐式类型转换',在泛型编程的世界里,这是否也成立呢?下面我
们来稍微修改一下前面这款例子中的代码,然后验证一下是这句话是否依然成立.
template<typename T>
class Rational{
public:
Rational(const T& numberator = 0, const T& denominator = 1);
const T numerator()const;
const T denominator()const;
...
};
template<typename T>
const Rational<T> operator*(const Rational<T>& left_handle_side,
const Rational<T>& right_handle_side );
我们希望此时的它能够一往无前的支持混合式算术运算,我沙沙地写出下面两行代码进行测试:
Rational<int> one_half( 1, 2 );
Rational<int> result = one_half * 2; //compile error.
咦?奇怪,难道换成了模板就不行了?事实确实如此!在条款24中编译器能够知道我们在尝试调用哪个函数(就是接
受两个Rationals参数的那个operator*啦),但在这里编译器却不知道我们要调用哪个函数,它们试图想出什么函数被
名为operator*的template具现化出来.但现在的问题是它们没有足够大的能耐来完成如此的工作.为了完成具现化工
作,必须先算出T是什么,于是它们开始了下面的尝试:
编译器首先看到了operator*的两个参数,它们的类型分别是Rational<int>(one_half类型)和int(2的类型),每个参
数分开考虑.以one_half进行推导比较容易,operator*的第一个参数声明为Rational<T>,而传递给函数的第一实参类
型是Rational<int>,故T一定int.但到了第二个参数推导的时候,问题就来了.operator*声明的第二个参数为
Rational<T>,但实参确实int类型(2).编译器如何推算T?会不会发生像条款24出现的隐式参数转换呢?(编译器适用
Rational<int>的non-explicit构造函数将2转换为Rational<int>,进而将T推导为int),但事实却是很惨酷的:它们没有
那样做.因为在template实参推导过程中从不将隐式类型转换函数纳入考虑.这下麻烦了,那有什么办法能够解决这一
问题的呢?别急,其实我们只需要利用一个事实就可以缓和编译器在template实参推导方面受到的挑战,这个事实就是
:template class内的friend声明式可以指涉某个特定的non-member函数.class templates并不依赖template实参推导
(后者只施行于function templates身上),所以编译器总是能够在class Rational<T>具现化时得知T.因此,我们的问题
的解决方案就出来了:令Rational<T> class声明适当的operator*为其friend函数.
template<typename T>
class Rational{
public:
... //同上
friend const Rational operator* ( const Rational& left_handle_side,
const Rational& right_handle_side );
};
template <typename T>
const Rational<T> operator*(const Rational<T>& left_handle_side,
const Rational<T>& right_handle_side )
{...}
现在对operator*的混合式调用就可以编译了,因为当对象one_half被声明为Rational<int>,class Rational<int>
就被具现化出来了,而作为过程的一部分,friend函数operator*也就被自动声明出来了,此时后者为一个具现的函数
而不是函数模板罗,因此编译器可在调用它时使用隐式转换,于是混合式调用就基本成功了!基本?难道还有未完成的
工作?呵呵,当你编译的时候没问题,是吧?你试试链接呢?竟然无法连接!!SHIT!怎么个情况????
现在我们回头来思考这个问题.混合式代码通过了编译是因为编译器知道我们要调用哪个函数,但那个函数只被
声明与Rational内,并没有被定义出来.而我们意图令此class外部的operator* template提供定义式,但是行不通----
----如果我们自己声明了一个函数,就有责任定义那个函数.既然我们没有提供定义式,连接器当然找不到它!
最简单的可行方法就是将operator*函数本体合并至其声明式内:
template<typename T>
class Rational{
public:
... //同上
friend const Rational operator*(const Rational& left_handle_side,
const Rational& right_handle_side ){
return Rational( left_handle_side.numerator() * right_handle_side.numerator(),
left_handle_side.denominator() * right_handle_side.denominator() );
}
};
简单的Build一下,我们就可以看到,对operator*的混合调用现在可以编译连接并执行了.哦也!operator*定义体放
到了类的内部成了inline函数,而inline声明会给类带来冲击.为了最小化这种冲击,我们可以令operator*不做任何事
情,只调用一个定义于class外部的辅助函数,当然,对本条款中的例子而言,这样做没有太大的意义,因为operator*只
是一个单行函数,但对于更复杂的函数而言,这样做也许就有价值.本款的例子典型长成这样:
template<typename T> class Rational;
template<typename T>
const Rational<T> doMultiply(const Rational<T>& left_handle_side,
const Rational<T>& right_handle_side);
template<typename T>
class Rational{
public:
...
friend const Rational<T> operator*(const Rational<T>& left_handle_side,
const Rational<T>& right_handle_side){
return doMultiply( left_handle_side, right_handle_side );
}
...
};
好了,今天的讨论就到这里!
请记住:
■ 当我们编写一个class template,而它所提供之'于此template相关的'函数支持'所有参数之隐式类型转换'时,请将
那些函数为'class template内部的friend函数'.