条款46: 需要类型转换时请为模板定义非成员函数
Define non-member functions inside template when type conversions are desired.条款24讨论过为什么 唯有non-member函数才有能力"在所有实参身上实施隐式类型转换",该条款并以Rational class 的 operator*函数为例.(强烈建议先熟悉 条款24的例子,operator*成为non-member函数,编译器只可以在实参上执行隐式类型转换).本条款首先以一个改动扩充 条款24的讨论:本条款将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)
{ ... }
就像
条款24一样,希望支持混合式(mixed-mode)算数运算,所以希望以下代码顺利通过编译.也预期它会,因为它正是
条款24所列的同一份代码,唯一不同的是Rational和 operator*如今都成了 template:
Rational<int> oneHalf(1, 2);
Rational<int> result = oneHalf * 2; // error!无法通过编译
上述失败的启示是,模板化的Rational内的某些东西似乎和其non-template 版本不同.事实上,确实如此.
条款24中,编译器知道尝试调用什么函数(就是接受两个Rational参数的那个 operator*),但这里编译器不知道想要调用哪个函数.取而代之的是,它们试图想出什么函数被名为 operator*的 template 具现化出来.它们知道它们应该具现化某个"名为operator*并接受两个Rational<T>参数"的函数,但为完成这一具现化行为,必须先算出T是什么,问题是它并没有能力知道.
为了推导T,它们看了看 operator*调用动作中的实参类型.本例中那些类型分别是Rational<int>(oneHalf的类型)和 int(2的类型).每个参数分开考虑.
以oneHalf进行推导,operator*的第一参数被声明为Rational<T>,而传递给 operator*的第一实参(oneHalf)的类型是Rational<int>,所以T一定是 int.其他参数的推导则没有这么顺利.在调用一个函数之前,首先必须知道那个函数存在,而为了知道它,必须先为相关的function template 推导出参数类型(然后才可将适当的函数具现化出来).然而 template 实参推导过程中并不考虑采纳"通过构造函数而发生的"隐式类型转换. 条款24不涉及 template,所以 template 实参推导不成为讨论议题.现在却是处于 template part of C++(详见 条款1)领域内,template 实参推导是关键议题.
只要利用一个事实,就可以缓和编译器在 template 实参推导方面受到的挑战: template class 内的 friend 声明式可以指涉某个特定函数.那意味着 class Rational<T>可以声明 operator*是它的一个 friend 函数. class template 并不依赖 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*(cnst Rational<T>& lhs, const Rational<T>& rhs)
{ ... }
现在对 operator*的混合式调用可以通过编译了,因为当对象oneHalf被声明为一个Rational<int>,class Rational<int>于是被具现化出来,
而作为过程的一部分,friend 函数 operator*(接受Rational<int>参数)也就被自动声明出来.后者身为一个函数而非模板函数(function template),因此编译器可在调用它时使用隐式转换函数(例如Rational的non-explicit构造函数),而
这便是混合式调用之所以成功的原因.
虽然混合式代码通过了编译,因为编译器知道要调用哪个函数(就是接受一个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函数(详见 条款24); 为了令整个函数被自动具现化,需要将它声明在 class 内部;而 在 class 内部声明non-member函数的唯一办法就是:令它成为一个 friend .因此就这样了.
如 条款30所述,定义于 class 内的函数都暗自成为 inline,包括像 operator*这样的 friend 函数.可以将这样的 inline 声明所带来的冲击最小化,做法是令 operator*不做任何事情,只调用一个定义于 class 外部的辅助函数.
注意:
当编写一个 class template,而它所提供的"与此template相关的"函数支持"所有函数的隐式类型转换"时,请将那些函数定义为"class template内部的friend函数".