1.前言
条款24讨论过为什么只有non-member函数才有能力“在所有实参身上实施隐式类型转换”,该条款并以Rational class的operator*函数为例operator*函数为例。而本条款的改变在于将Rational和operator*模板化了:
template<typename T>
class Rational{public:Rational(const T& numerator[=0,const T& denominator=1);const T numperator() const;const T denominator() const;....
};
template<typename T>
const Rational<T> operator* (const Rational<T>& lhs,const Rational<T>& rhs)
{...
}
2.实例分析
就像条款24,我们也希望支持混合式(Mixed-mode)算数运算,所以我们希望以下代码顺利通过编译,唯一不同的时Rationl和operator*如今都成了templates:
Rational<int> oneHalf(1,2);//该例子来自条款24,唯一不同的是Rational改为template
Rational<int> result=oneHalf*2;//错误,无法通过编译
上述失败给我们的启示是模板化的Rational内的某些东西和其non-template版本不同。在条款24内,编译器知道我们尝试调用什么函数(即接受两个Rationals参数的那个operator),但这里编译器不知道我们想要调用哪个函数。取而代之的是它们试图想出什么函数被名为operator*的template具现化出来。它们知道他们应该具现化某个“名为operator*”并接受两个Rational<T>参数”的函数,但为完成这一具现化行动,必须先算出T是什么。
为了推导T,它们看了看operator*调用动作中的实参类型。本例中那些类型分别为Ratioanl<int>(oneHalf的类型)和int(的类型)。
以oneHalf进行推导,operator*的第一参数被声明为Rational<T>,而传递给operator*的第一实参(oneHalf)的类型是Rational<T>,所以T一定是int,其它参数的推倒则没有这么顺利。operator*的第二参数被声明为Rational<T>,但传递给operator*的第二实参(2)类型是int。
只要利用一个事实,我们就可以缓和编译器在template实参推导方面受到的挑战:template class内的friend声明式可以指涉某个特定函数。哪意味着class Rational<T>可以声明operator*是它的一个friend函数。class template并不依赖template实参推导(后者只施行于function template身上),所以编译器总是能够在class Rational<T>具现化时得知T,因此,令Rational<T> class声明适当的operator*为其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)
{...
}
现在对operator*混合式的调用可以通过编译了,因为当对象oneHalf被声明为一个Rational<int>,class Rational<int>于是被具现化出来,而作为过程的一部分,friend函数operator*(接受Rational<int>参数)也就被自动声明出来。后者身为一个函数而非函数模板(function template),因此编译器可在调用它时使用隐式转换函数(例如Rational的non-exolicit构造函数),而这便是混合式调用之所以成功的原因。
虽然这段代码通过了编译,但是却无法连接。首先我们对Rational内声明的operator*语法进行讨论。
在一个class template内,template名称可被用来作为"template和其参数"的简略表达式,所以在Rational<T>内我们可以只写Rational而不必写Rational<T>。本例中的operator*被声明为接受并返回Rational,如果它被声明如下,一样有效:
template<typename T>
class Rational{public:...friendconst Rational<T> operator*(const Rational<T>& lhs,const Rational<T>& rhs);....
};
然而使用简略的表达式比较轻松,也比较普遍。
现在回头想想出现的问题。混合式代码通过了编译,因为编译器知道我们要调用哪个函数(就是接受一个Rational<int>以及又一个Rational<int>的那个operator*),但那个函数只被声明于Rational内,并没有被定义出来,我们意图令此class外部的operator* template提供定义式,但是行不通-如果我们自己声明了一个函数(即Rational template内的作为),就有责任定义那个函数。既然我们没有提供定义式,连接器自然找不到它们。
或许最简单的办法就是将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());}
}
这便如同我们所期望地正常运作起来:对operator*地混合式调用现在可编译连接并执行。
这项技术地有个有趣点是:虽然使用了friend,却与friend地传统用途“访问class的non-public成分”毫不相干,为了让类型转换可能发生于所有实参上面,我们需要一个non-member函数:为了令这个函数被自动具现化,我们需要将它声明在class内部;而在class内部声明non-member函数的唯一办法就是:令它成为一个friend。
正如条款30所说,定义于class内的函数都暗自成为Inline,包括像operator*这样的friend函数,可以将这样的inline声明所带来的冲击最小化,做法是令operator*不做任何事情,只调用一个定义于class外部的辅助函数。在本条款的例子中,这样做的意义不大,因为operator*已经是个单行函数,但对更复杂的函数而言,那么做也许就有价值。“令friend函数调用辅助函数”的做法的确值得研究。
“Rational是个template”这一事实意味着上述的辅助函数通常也是个template,所以定义lRational的头文件代码,比如:
template<typename T> class Rational;//声明Rational template
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs,const Rational<T>& rhs);
template<typename T>
class Rational{public:...friend const Rational<T> operator*(const Rational<T>& lhs,const Rational<T>& rhs){return doMultiply(lhs,rhs);}...
};
许多编译器实质上会强迫你把所有template定义式放进头文件内,所以你或许需要在头文件内定义doMultiu\ply,比如以下例子:
template<typename T>
const Rational<T> domultiply(const Rational<T>& lhs,const Rational<T>& rhs)
{return Rational<T>(lhs.numerator*rhs.numerator(),lhs.denominator()*rhs.denominator());
}
作为一个template,doMultiply不支持所以混合式乘法,但它其实不需要。它只被operator*调用,而operator*支持了混合式操作,本质上operator*支持了类型转换所需的任何东西,确保两个Rational对象能够相乘,然后它将这两个对象传给一个适当的doMultiply template具现体,完成实际的乘法操作。
3.总结
当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所以参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”。