• Effective C++条款24:若所有参数皆需类型转换,请为此采用non-member函数


    Effective C++条款24:若所有参数皆需类型转换,请为此采用non-member函数(Declare non-member functions when type conversions should apply to all parameters)


    《Effective C++》是一本轻薄短小的高密度的“专家经验积累”。本系列就是对Effective C++进行通读:


    条款24:若所有参数皆需类型转换,请为此采用non-member函数

    1、错误案例——将需要隐式类型转换的函数声明为成员函数

      使类支持隐式转换是一个糟糕的想法。当然也有例外的情况,最常见的一个例子就是数值类型。

      举个例子,如果你设计一个表示有理数的类,允许从整型到有理数的隐式转换应该是合理的。在C++内置类型中,从int转换到double也是再合理不过的了(比从double转换到int更加合理)。看下面的例子:

    class Rational
    {
    public:
        //构造函数未设置为explicit,因为我们希望一个int可以隐式转换为Rational
        Rational(int numerator = 0, int denominator = 1);
        int numerator()const;
        int denominator()const; 
        const Rational operator*(const Rational& rhs)const;
    private:
        ...
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

      你想支持有理数的算术运算,比如加法,乘法等等,但是你不知道是由成员函数还是非成员函数,或者非成员友元函数来实现它们。你的直觉会告诉你当你犹豫不决的时候,你应该使用面向对象的精神。有理数的乘积和Rational类相关,就会很自然认为该在Rationl类内为有理数实现operator*。条款23曾反直觉的主张,将函数放进相关 class 内有时会与面向对象守则发生矛盾,现在我们将其放到一边,研究一下将operator*实现为Rational成员函数的做法:

    class Rational {
    public:
    	...
    	const Rational operator*(const Rational& rhs) const;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

      (如果你不明白为什么函数声明成上面的样子——返回一个const value值,参数为const引用,参考[条款3],条款20条款21

    这个设计让你极为方便的执行有理数的乘法:

    Rational oneEighth(1, 8);
    Rational oneHalf(1, 2);
    Rational result = oneHalf*oneEighth;//正确
    result = result*oneEighth;          //正确
    
    • 1
    • 2
    • 3
    • 4

      但是你不满足。你希望可以支持混合模式的操作,例如可以支持int类型和Rational类型之间的乘法。这种不同类型之间的乘法也是很自然的事情。

      当你尝试这种混合模式的运算的时候,你会发现只有一半的操作是对的:

    Rational res = oneHalf * 2;//正确
    Rational result = 2 * oneHalf; //错误
    
    • 1
    • 2

      那么哪里出错了呢???

    2、混合运算错误原因分析

      将上面的例子用等价的函数形式写出来,你就会知道问题出在哪里:

    result = oneHalf.operator*(2); // fine
    result = 2.operator*(oneHalf ); // error!
    
    • 1
    • 2

      oneHalf对象是Rational类的一个实例,而Rational支持operator操作,所以编译器能调用这个函数。然而,整型2却没有关联的类,也就没有operator成员函数。编译器同时会去寻找非成员operator*函数(也就是命名空间或者全局范围内的函数):

    result = operator*(2, oneHalf ); // error!
    
    • 1

      但是在这个例子中,没有带int和Rational类型参数的非成员函数,所以搜索会失败。

      再看一眼调用成功的那个函数。你会发现第二个参数是整型2,但是Rational::operator*使用Rational对象作为参数。这里发生了什么?为什么都是2,一个可以调用另一个却不能调用?

      是的,这里发生了隐式类型转换。编译器知道函数需要Rational类型,但你传递了int类型的实参,它们也同样知道通过调用Rational的构造函数,可以将你提供的int实参转换成一个Rational类型实参,这就是编译器所做的。编译器的做法就像下面这样调用:

    const Rational temp(2); // 创建一个临时变量
    result = oneHalf * temp; // 等同于oneHalf.operator*(temp);
    
    • 1
    • 2

      当然,编译器能这么做仅仅因为类提供了non-explicit构造函数。如果Rational类的构造函数是explicit的,下面的两个句子都会出错:

    result = oneHalf * 2; // error! 在显示构造的情况下
    result = 2 * oneHalf; // 一样的错误,一样的问题
    
    • 1
    • 2

      这样就不能支持混合模式的运算了,但是至少两个句子的行为现在一致了。

      然而你的目标是既能支持混合模式的运算又要满足一致性,也就是,你需要一个设计使得上面的两个句子都能通过编译。回到上面的例子,当Rational的构造函数是non-explicit的时候,为什么一个能编译通过另外一个不行呢?

      看上去是这样的,只有参数列表中的参数才有资格进行隐式类型转换。而调用成员函数的隐式参数,this指针指向的那个,绝没有资格进行隐式类型转换。这就是为什么第一个调用成功而第二个调用失败的原因。

    3、以非成员函数版本代替成员函数版本

    为什么设计非成员函数版本:

      从上面的“一”我们可以看到,如果operator*()为成员函数,在某些情况下即使存在隐式转换也不能成功执行

    因此我们可以将成员函数版本改为非成员函数版本(见下面详细介绍)
    例如下面将operator*()函数变为一个非成员函数。代码如下:

    class Rational
    {
    public:
        Rational(int numerator = 0, int denominator = 1);
        int numerator()const;
        int denominator()const;
    private:
       	...
    };
     
    const Rational operator*(const Rational& lhs,const Rational& rhs)
    {
        return Rational(lhs.numerator()*rhs.numerator(),
            lhs.denominator()*rhs.denominator());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    Rational oneFourth(1, 4);
    Rational result;
    result = oneFourth* 2;
    result = 2 * oneFourth;
    
    • 1
    • 2
    • 3
    • 4

      这很nice,不过还有点要注意:operator* 是否该成为Rational class的一个友元函数呢?

      答案是否定的,因为 operator* 可以完全依靠Rational的public接口来实现。上面的代码就是一种实现方式。我们能得到一个很重要的结论:成员函数的反义词是非成员函数而不是友元函数。太多的C++程序员认为一个类中的函数如果不是一个成员函数(举个例子,需要为所有参数做类型转换),那么他就应该是一个友元函数。上面的例子表明这样的推理是有缺陷的。尽量避免使用友元函数,就像生活中的例子,朋友带来的麻烦可能比从它们身上得到的帮助要多。

    在这里插入图片描述

    4、牢记

    • 如果你需要为某个函数的所有参数(包括被this这孩子很所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member。

    总结

    期待大家和我交流,留言或者私信,一起学习,一起进步!

  • 相关阅读:
    全面揭秘!微信传输助手的用处有哪些!
    基于开源ERDDAP的海洋学科数据分发技术简介
    【每天学会一个渗透测试工具】dirsearch安装及使用指南
    微软:我已把显存优化做到了极致,还有谁?
    UPC2022暑期个人训练赛第19场(B,P)
    java的xml文件中“http://www.springframework.org...“报红问题
    防火墙——计算机网络
    Zookeeper安装(单机、伪集群、多机集群)
    java毕业设计项目选题springboot汉赛汽车租赁平台
    算法竞赛进阶指南 0x21 树与图的遍历
  • 原文地址:https://blog.csdn.net/CltCj/article/details/128158347