Effective C++ 条款46 需要类型转换时请为模板定义非成员函数
1. 条款24举出一个Rational的例子,来说明为什么只有non-member函数才有能力"在所有实参身上实施隐式类型转换".Rational的定义如下:

class Rational{ public: Rational(int numerator=0,int denominator=1); int numerator()const; int denominator()const; private: int numerator; int denominator; };
operator*声明为Rational的non-member函数:
const Rational operator*(const Rational& lhs,const Rational& rhs);
在未涉及到模板时,这么做是正确的,如果将Rational升级为类模板,可能像这样:

template<typename T> class Rational{ public: Rational(const T& numerator=0,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){ ... }
那么对于以下代码:
Rational<int> oneHalf(1,2); Rational<int> result=onHalf*2;
看起来"onHalf*2"似乎应该使函数模板operator*具现化并调用它,但实际上编译不通过,根本原因在于编译器"在template实参推到过程中从不将隐式转换纳入考虑":转换函数在函数调用过程中的确被使用(如果operator*是一个函数而不是函数模板的话),但在调用一个函数之前,必须要知道那个函数存在,而为了知道它,必须先为相关的function template推导出参数类型(然后才可将适当的函数具现化出来).然而template实参推导过程中并不考虑采纳"通过构造函数而发生的"隐式类型转换!
2. 只要利用一个事实就可以改变这种现状:template class内的friend声明式可以指涉该特定函数.也就是说Rational可以声明operator*为它的一个friend函数,class template并不依赖于template实参推导,因此编译器总是能够在class Rational具现化时得知T,因此,令Rational<T> class声明适当的operator*为其friend函数可简化整个问题:

template<typename T> class Rational{ public: friend 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被声明时,class Rational<int>被具现化出来,而作为过程的一部分,friend函数operator*也就被作为一个函数而非函数模板自动声明出来,因此编译器可以在调用它时使用隐式转换函数.
此时还未结束,以上代码虽然通过了编译,但却无法链接——Rational<int> operator*(const Rational<int>&lhs,const Rational<int>&rhs)已经被声明出来,但却没有定义.使用template是行不通的,因为此当Rational被具现化时,operator*只是作为一个普通的函数声明被具现化出来,编译器不会认为它和operator*函数模板有关联而为它具现化出一个函数实体.
解决办法就是将operator*函数本体合并至其声明式内:

template<typename T> class Rational{ public: friend Rational operator*(const Rational& lhs, const Rational& rhs){ return Rational(lhs.numerator()*rhs.numerator(), lhs.denominator()*rhs.denominator()); } ... };
此时对operator*的调用可编译链接并执行.
3. 在本条款中,friend的作用不再是提升函数或类的访问权限,而是使类型转换发生在所有参数身上:为了使该函数被自动具现化,需要把它声明在类内部,而在类内部声明non-member函数的唯一办法就是令它成为一个friend.
由于operator*需要在Rational内部定义,它被默认声明为inline,为了使这种inline声明带来的冲击最小(本例中operator*已经是一个单行函数,但更复杂的函数也许需要这样),可以使它调用一个定义域class外部的辅助函数,由该辅助函数完成实际功能:

template<typename T> const Rational<T> doMultiply(const Rational& lhs, const Rational& rhs){ ... } template<typename T> class Rational{ public: friend Rational operator*(const Rational& lhs, const Rational& rhs){ doMultiply(lhs,rhs); ... };
许多编译器为了实行template具现化,要求把template定义式放在头文件内,因此可能需要在头文件内定义doMultiply,但doMultiply可以不为inline.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】