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

一. 内容

  1. 条款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; //错误
    }
    
  2. 上述代码和条款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 是什么类型才能被具现化

  3. 一个比较好的解决方法是:在 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);
        //...
    };
    
  4. 至于为什么链接出错呢?是因为编译器虽然知道要调用这个函数,但该函数只被用 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

  5. 一如条款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());
    }
    

二. 总结

  1. 当我们编写一个 class template,而它所提供之与此 template 相关的函数支持所有参数隐式类型转换时,请将那些函数定义为 class template 内部的 friend 函数。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值