条款24:若所有参数皆需类型转换,请为此采用non-member函数
通常情况,class不应该支持隐式类型转换,因为这样可能导致我们想不到的问题。这个规则也有例外,最常见的例外是建立数值类型时。例如编写一个有理数管理类,允许整数隐式类型转换为有理数似乎颇为合理。
class Rational{
public:
Rational(int numerator=0, int denominator=1);//非explicit,允许隐式转换
……
};
如果要支持加减乘除等运算,这时重载运算符时是应该重载为member函数还是non-member函数呢,或者non-member
friend函数?如果写成member函数:
class Rational{
public:
……
const Rational operator*(const Rational& rhs);
……
};
这样编写可以 使得将两个有理数以最轻松自在的方式相乘:
Rational onEight(1,8);
Rational oneHalf(1,2);
Rational result=onEight*oneHalf;
result=result*onEight;
如果进行混合运算,却只有一半行得通:result=oneHalf*2;//正确,相当于oneHalf.operator*(2);
result=2*oneHalf;//错误,相当于2.operator*(oneHalf);
oneHalf是一个内含operator*函数的class对象,所以编译器调用该函数。然而整数2并没有对应的class,也就没有operator*成员函数。编译器也会尝试寻找可被以下这般调用的non-member operator*(也就是在命名空间内或在global作用域内):
result=operator*(2,oneHalf); //错误
但本例不存在这样一个接受int和Rational作为参数的non-member operator*,因此查找失败。
前面那个正确。注意其第二个参数是整数2,但
const Rational operator*(const Rational& rhs);
需要的实参是个Rational对象。这里发生了所谓的隐式类型转换。编译器知道只要调用Rational构造函数并赋予所提供的int,就可以变出一个适当的Rational来。如果构造函数是下面这样:explicit Rational(int numerator=0, int denominator=1);//explicit,禁止隐式转换
则上面也会出错。
注意:只有当参数被列于参数列内时,这个参数才是隐式转换的合格参与者。
第二次调用的2不是位于参数列内,所以不能因式转换为一个Rational对象,也就没有operator*函数了。
如果要支持混合运算,可以让operator*成为一个non-member函数,这样编译器可以在实参身上执行隐式类型转换。
const Rational operator*(const Rational& lhs, const Rational& rhs);
这样就可以进行混合运算了。那么还有一个问题就是,是否应该是operator*成为friend函数。如果可以通过public接口,来获取内部数据,那么可以不是friend函数,否则,如果读取private数据,那么要成为friend函数。这里还有一个重要结论:member函数的反面是non-member函数,不是friend函数。如果可以避免成为friend函数,那么最好避免,因为friend的封装低于非friend。当需要考虑template时,让class变为class template时,又有一些新的解法。这个在后面条款46有讲到。
总结:如果需要为某个函数的所有参数(包括this指针所指向的隐喻参数)进行类型转换,这个函数必须是个non-member函数
条款46:需要类型转换时请为模板定义non-member函数
条款 24提到过为什么non-member函数才有能力“在所有实参身上实施隐式类型转换”,本条款接着那个Rational例子来讲,把Rational class模板化:
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)
{……};
Rational<int> oneHalf(1,2);
Rational<int> result=oneHalf*2;//错误,无法通过编译
非模板的例子可以通过编译,但是模板化的例子就不行。在*条款**24,编译器直到我们尝试调用什么函数(就是接受两个Rational参数那个operator ),但是这里编译器不知道。编译器试图想什么函数被命名为operato* 的template具体化出来,它们知道自己可以具体化某个operator* 并接受两个Rational参数的函数,但为完成这一具体化行动,必须先算出T是什么。问题是它们没这个能耐。
看一下这个例子,编译器怎么推导T。本例中类型参数分别是Rational和int。operator* 的第一个参数被声明为Rational,传递给operator* 的第一实参(oneHalf)正类型正是Rational,所以T一定是int。operator* 的第二个参数类型被声明为Rational,但传递给 operator* 的第二个实参类型是int,编译器如何推算出T?或许你期望编译器使用Rational的non-explicit构造函数将2转换为Rational,进而推导出T为int,但它不这么做,因为在template实参推导过程中从不将隐式类型转换考虑在内。隐式转换在函数调用过程中的确被使用,但是在能够调用一个函数之前,首先要知道那个函数的存在;为了知道存在,必须先为相关的function template推导出参数类型(然后才可以将适当的函数具体化出来)。但是在template实参推导过程中不考虑通过构造函数发生的隐式类型转换。
现在解决编译器在template实参推导方面遇到的挑战,可以使用template class内的friend函数,因为template class内的friend声明式可以指涉某个特定的函数。也就是说class Rational可以说明operator* 是它的friend函数。class templates并不依赖template实参推导(后者只施行于function templates身上),所以编译器总是能够在class Rational具体化时得知T。所以令Rational 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,class Rational被具体化出来,而作为过程的一部分,friend函数operator* (接受Rational参数)也就自动声明出来。后者身为一个函数而非函数模板,因此编译器在调用它的时候使用隐式转换(将int转换为Rational),所以混合调用可以通过编译。虽然通过编译,但是还会有链接问题,这个稍后再谈。先来看一下Rational内声明operator *的语法。
在一个class template内,template名称可被用来作为template和其参数的简略表达方式,所以在Rational内,我们可以把Rational简写为Rational。如果像下面这样写,一样有效
template<typename T>
class Rational{
public:
……
friend const Rational operator*(const Rational<T>& lhs,const Rational<T>& rhs);//声明
};
现在回头看一下刚刚说的链接的问题。虽然编译器直到我们调用的函数是接受Rational的那个operator * ,但是这个函数只有声明,没有定义。我们本来想让此class外部的operator * 提供定义式,但是这样行不通。如果我们自己声明了一个函数(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());
}
};
这个技术虽然使用了friend,却与传统的friend用途“访问class的non-public成员”不同。为了让类型转换可能发生与所有实参身上,我们需要一个non-member函数(**条款**24);为了让这个函数被自动具体化,我们需要将它声明在class内部;而在class内部声明non-member函数的唯一办法就是让它成为一个friend。
定义在class内部的函数都是inline函数,包括像operator * 这样的friend函数。为了将inline声明带来的冲击最小化,可以让operator * 调用定义在class外部的辅助函数。
template<typename T> class Rational;//forward decelarion
template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs,const Rational<T>& rhs);
template<typename T>
class Rational{
public:
……
friend const Rational operator*(const Rational& lhs,const Rational& rhs);//声明+定义
{
return doMultiply(lhs,rhs);
}
};
许多编译器会强迫你把template定义式放到头文件,所以有时你需要在头文件定义doMultiply
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());
}
doMultiply是个template,自然不支持混合乘法,其实也没必要支持。它只是被operator * 调用,operator * 支持了混合乘法。
总结
- 当编写一个class template时,它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为class template内部的friend函数。