47.在需要类型转换时请为模板定义非成员函数

前言

 
Effective C++ 25已经描述过为何仅有non-member函数才有能力对所有实参执行隐式转换。本节我们将类型转换拓展到template范围。


问题实例

 
在上一次的实例中,我们以Rational class与operator*讨论了在所有实参身上执行隐式转换。本次实例则将它们模板化。

1
2
3
4
5
6
7
8
9
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) {}

我们预期以下代码也会通过编译,因为它们在非模板时确实编译成功了:
1
2
Rational<int> oneHalf(1,2);
Rational<int> result = oneHalf *2;//error 编译失败

我们应该能够意识到,模板化的Rational中的某些东西似乎与non-template版本不同。


问题剖析

 
在非模板情况下,编译器知道我们尝试去调用接受两个Rational参数的operator*,但在这里,编译器不知道我们想调用哪个函数。
它在试图想出什么函数可以被名为operator*的template具现化出来。它知道它们应该可以具现出某个名为operator*且接受两个rational<T>参数的函数,但为了完成这一具现化行为,必须先算出T是什么。编译器无法做到。

实参推导

1
2
Rational<int> oneHalf(1,2);
Rational<int> result = oneHalf *2;

operator*的第一参数被声明为rational<T>,而传递给operator*的第一实参oneHalf是Rational<int>,那可以肯定T是int.关键在于第二实参接受的应该是一个Rational<int>,但实际传入的却是int。T的推导无法确定。
template实参推导过程中并不考虑隐式类型转换。这就是报错的关键。尽管隐式转换在函数调用过程中的确被使用,但在能调用一个函数之前,它必须已经存在(已被推导出并具现化)。


问题解决

friend声明

template class内的friend声明式可以指涉某个特定函数,那意味着class Rational<T>可以声明operator*是它的一个friend函数。class templates并不依赖于template实参推导(实参推导仅发生在function templates身上),所以编译器总是能在class rational<T>具现化时得知T.因此,令Rational<T> class 声明适当的operator*为其friend函数,可以简化整个问题:

1
2
3
4
5
6
7
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);

当oneHalf被声明为一个Rational<T>时,rational<int>则被具现化,而作为具现化的一部分,operator*也完成了自动声明,后者作为一个non-template function,编译器可以在调用时使用隐式转换。

连接

friend声明帮助我们完成了编译,但上述代码并不能连接,原因很简单,只有声明是而没有定义式。我们也无法在class外部完成operator* template定义,因为既然在Rational template中声明,就必须在其中定义。因此必须把函数本体移动到声明式中:

1
2
3
4
5
6
7
8
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());//返回值优化
}
};

至此,编译并连接成功,混合式计算正常运行。


解决方案剖析

 
这项技术的关键点在于:我们使用friend并不是因为我们想访问non-public成员。我们为了让类型转换可以用于所有实参,需要一个non-member函数。为了令它自动具现化,我们需要它位于class内部。综合二者,我们得到了friend.

众所周知,定义在class内部的函数都申请inline,包括friend函数。为了避免可能带来的高成本,一般而言,我们会令它不做任何事情,只是调用class外的一个辅助函数。

辅助函数

Rational是一个template意味着上述的辅助函数通常也是一个template。我们定义了Rational的头文件代码如下:

1
2
3
4
5
6
7
8
9
template<typename T> class Rational;
template<typename T> const Rational<T> doMutiply(const Rational<T> &,......);
template <typename T>
class Rational{
public:
friend const rational operator*(const Rational& lhs,const Rational& rhs){
return doMutiply(lhs,rhs);
}
};


总结

 
当我们编写一个class template,而它提供的“与此template相关”函数需要“所有参数支持隐式转换时”,请将那些函数定义为friend函数,并在class template中定义它们。