需要类型转换时请为模板定义非成员函数——条款46

探讨了在模板化Rational类及其乘法运算符中实现混合算术运算的挑战。通过将运算符定义为类模板的友元函数,解决了编译器在模板实参推导方面的难题,允许在所有实参上实施隐式类型转换。

        条款24讨论过为什么唯有non-member函数才有能力“在所有实参身上实施隐式类型转换”,该条款并以Rational class的operator*函数为例。我强烈建议你继续看下去之前先让自己熟稔那个例子,因为本条款首先以一个看似无害的改动扩充条款24的讨论:本条款将Rational和operator*模板化了:

template<typename T>
    class Rational {
    public:
        Rational(const T& numberator = 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一样,我们希望支持混合式算数运算,所以我们希望以下代码顺利通过编译。我们也预期它会,因为它正是条款24所列的同一份代码,唯一不同的是Rational和operator*如今都成了templates:

Rational<int> oneHalf(1, 2);
Rational<int> result = oneHalf * 2;  // 错误!无法通过编译

        上述失败给我们的启示是,模板化的Rational内的某些东西似乎和其non-template版本不同。事实的确如此。在条款24内,编译器知道我们尝试调用什么函数(就是接受两个Rationals参数的那个operator*),但这里编译器不知道我们想要调用哪个函数。取而代之的是,它们试图想出什么函数被名为operator*的template具现化出来。它们知道它们应该可以具现化某个“名为operator*并接受两个Rational<T>参数”的函数,但为完成这一具现化行动,必须先算出T是什么。问题是它们没这个能耐。

        为了推导T,它们看了看operator*调用动作中的实参类型。本例中那些类型分别是Rational<int>(oneHalf的类型)和int(2的类型)。每个参数分开考虑。

        以oneHalf进行推导,过程并不困难。operator*的第一个参数被声明为Rational<T>,而传递给operator*的第一实参(oneHalf)的类型是Rational<int>,所以T一定是int。其他参数的推导则没有这么顺利。operator*的第二参数被声明为Rational<T>,但传递给operator*的第二实参(2)类型是int。编译器如何根据这个推算出?你或许会期盼编译器使用Rational<int>的non-explicit构造函数将2转换为Rational<int>,进而将T推导为int,但它们不那么做,因为在template实参推导过程中从不将隐式类型转换函数纳入考虑。

        只要利用一个事实,我们就可以缓和编译器在template实参推导方面受到的挑战:template class内的friend声明式可以指涉某个特定函数。那意味Rational<T>可以声明operator*是它的一个friend函数。Class templates并不依赖template实参推导(后者只施行于function templates身上),所以编译器总是能够在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*(const Rational<T>& lhs, const Rational<T>& rhs)
{ ... }

        现在对operator的混合式调用可以通过编译了,因为当对象 oneHalf被声明为一个Rational<int>, class Rational<int>于是被具现化出来,而作为过程的一部分,friend函数operator*(接受Rational<int>参数)也就被自动声明出来。后者身为一个函数而非函数模板,因此编译器可在调用它时使用隐式转换函数(例如Rational的non-explicit构造函数),而这便是混合式调用之所以成果的原因。

        但是,此情境下的“成功”是个有趣的字眼,因为虽然这段代码通过编译,却无法连接。稍后我马上回来处理这个问题,首先我要谈谈在Rational内声明operator*的语法。

        在一个class template内,template名称可被用来作为“template和其参数”的简略表达方式,所以在Rational<T>内我们可以只写Rational而不必写Rational<T>。本例中这只节省我们少打几个字,但若出现许多参数,或参数名称很长,这可以节省我们的时间,也可以让代码比较干净。我谈这个是因为,本例中的operator*被声明为接受并返回Rationals(而非Rational<T>s)。如果它被声明如下,一样有效:

template<typename T>
    class Rational {
    public:
        ...
    friend
        const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>& rhs);
        ...
};

        然而使用简略表达式比较轻松也比较普遍。

        现在回头想想我们的问题。混合式代码通过了编译,因为编译器知道我们要调用哪个函数,但哪个函数只被声明与Rational内,并没有被定义出来。我们意图令此class外部的operator* template提供定义式,但是行不通——如果我们自己声明了一个函数,就有责任定义那个函数。既然我们没有提供定义式,连接器当然找不到它!

        或许最简单的可行办法就是将operator*函数本体合并至其声明式内:

template<typename T>
    class Rational {
    public:
        ...
        friend
        const Rational operator*(const Rational& lhs, const Rational& rhs)
        {
            return Rational(lhs.numberator() * rhis.numberator(), lhs.denominator() * rhs.denominator());
        }
};

        这便如同我们所期望地正常运作了起来:对operator*的混合式调用现在可以编译并执行。

请记住

  • 当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”。
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值