一. 内容
-
条款24讨论了为什么只有 non-member 函数才有能力在所有实参身上实施隐式类型转换。在本条款下,我们将讨论的范围
扩展到 template 的领域
:template <typename T> class TRational { public: TRational(const T& mNumerator, const T& mDenominator): Numerator(mNumerator), Denominator(mDenominator) {} public: T GetNumerator() const { return Numerator; } T GetDenominator() const { return Denominator; } private: T Numerator; T Denominator; }; template <typename T> inline TRational<T> operator*(const TRational<T>& RationalOne, const TRational<T>& RationalTwo) { return TRational<T>(RationalOne.GetNumerator() * RationalTwo.GetNumerator(), RationalOne.GetDenominator() * RationalTwo.GetDenominator()); } inline void TryWithTRational() { const TRational<int> TempOne(1, 8); const TRational<int> TempTwo(1, 2); TRational<int> Result = TempOne * TempTwo; //很好 Result = Result * TempOne; //很好 Result = Result * 2; //错误 Result = 2 * Result; //错误 }
-
上述代码和条款24所列代码的区别就是加上了 template。就像条款24所期望的那样,我们希望支持混合式算术运算。我们期待这段代码也能通过编译并正确运行,然而编译器报错了,模板化带来了一些不同,导致编译器无法找到相应的函数进行调用。
实际上编译器试图想出什么函数被名为 operator* 的 template 具现化。它知道可以具现化某个名为 operator* 并接受两个 TRational<T> 的参数的函数,但
为了完成这个具现化的任务,它必须先算出 T 是什么
。问题就在于它没有这个能力。以 Result * 2 为例,Result 是一个类型为 TRational<int> 的参数,所以编译器可以得知 T 为 int 。其他的参数就没这样顺利,2 是一个 int ,编译器如何从 int 推算出 TRational<T> 的 T 是什么类型呢?你也许期待编译器用 TRational<int> 进行构造,但这是不行的,因为在 template 实参推导过程中:
从不将隐式类型转换函数纳入考虑
。因为相应的隐式转换函数也需要知道 T 是什么类型才能被具现化
。 -
一个比较好的解决方法是:
在 template class 内使用 friend 声明式指出特定函数
,这样编译器总是可以在 class TRational<T> 具现化时得知 T:template <typename T> class TRational { public: friend TRational<T> operator*(const TRational<T>& RationalOne, const TRational<T>& RationalTwo); //... }; template <typename T> inline TRational<T> operator*(const TRational<T>& RationalOne, const TRational<T>& RationalTwo) { return TRational<T>(RationalOne.GetNumerator() * RationalTwo.GetNumerator(), RationalOne.GetDenominator() * RationalTwo.GetDenominator()); } inline void TryWithTRational() { const TRational<int> TempOne(1, 8); const TRational<int> TempTwo(1, 2); TRational<int> Result = TempOne * TempTwo; //很好 Result = Result * TempOne; //很好 Result = Result * 2;//OK Result = 2 * Result; //Ok }
现在这段代码可以通过编译,但是仍会有
链接错误
,我们稍后再提。这段代码能通过编译是因为当对象 Result 被声明为一个 TRational<int> 时,
friend 函数 operator* 也通过 TRational<int> 被具现出来
,成为一个函数,不再是函数模板(function template),所以可以使用隐式转换函数。注意一个小技巧,当一个 class template 内,template 名称可以作为 template声明 的简略表达形式,所以在 TRational<T> 我们可以只写 Rational 而不必写 TRational<T>,对于有很多参数的 template,这样可以节省一些时间,并让代码看起来干净,当然为了一致性,意义也并不大:
template <typename T> class TRational { public: friend TRational operator*(const TRational& RationalOne, const TRational& RationalTwo); //... };
-
至于为什么链接出错呢?是因为编译器虽然知道要调用这个函数,但该函数只被用
friend 声明于 template 内,并没有实际定义
。可惜的是虽然声明式知道了 T 是 int,但是类外的函数模板仍不知道,因为它们并无实际关系。一个最简单可行的方法就是
将 operator* 的函数体从类外合并到 template 声明式中
:template <typename T> class TRational { public: TRational(const T& mNumerator =0, const T& mDenominator=1): Numerator(mNumerator), Denominator(mDenominator) {} public: T GetNumerator() const { return Numerator; } T GetDenominator() const { return Denominator; } friend TRational operator*(const TRational& RationalOne, const TRational& RationalTwo) { return TRational<T>(RationalOne.GetNumerator() * RationalTwo.GetNumerator(), RationalOne.GetDenominator() * RationalTwo.GetDenominator()); } private: T Numerator; T Denominator; }; inline void TryWithTRational() { const TRational<int> TempOne(1, 8); const TRational<int> TempTwo(1, 2); TRational<int> Result = TempOne * TempTwo; //很好 Result = Result * TempOne; //很好 Result = Result * 2;//编译ok,运行ok too Result = 2 * Result; //编译ok,运行ok too }
这样混合式运算的问题终于得到解决。
这项技术的趣味点是,虽然我们使用 friend 关键字,却和其传统用途:访问 class 的 non-public 成分不同。我们是
为了让类型转换发生在所有可能的实参上,我们需要一个 non-member 函数
,而为了使这个函数自动具现化,我们需要将它声明在 class 内部
,而在 class 内部声明 non-member 函数的唯一有效方法就是:令它成为一个 friend
。 -
一如条款30所说,
定义于 class 内的函数都将暗自 inline
,所以一个更好的做法是:令该 friend 函数调用另一个辅助函数:template <typename T> class TRational { public: friend TRational operator*(const TRational& RationalOne, const TRational& RationalTwo) { return OnMultiply(RationalOne,RationalTwo); } //... }; template <typename T> TRational<T> OnMultiply(const TRational<T>& RationalOne, const TRational<T>& RationalTwo){ return TRational<T>(RationalOne.GetNumerator() * RationalTwo.GetNumerator(), RationalOne.GetDenominator() * RationalTwo.GetDenominator()); }
二. 总结
- 当我们编写一个 class template,而它所提供之与此 template 相关的函数支持所有参数隐式类型转换时,请将那些函数定义为 class template 内部的 friend 函数。