• 读书笔记 effective c++ Item 24 如果函数的所有参数都需要类型转换,将其声明成非成员函数


    1. 将需要隐式类型转换的函数声明为成员函数会出现问题

    使类支持隐式转换是一个坏的想法。当然也有例外的情况,最常见的一个例子就是数值类型。举个例子,如果你设计一个表示有理数的类,允许从整型到有理数的隐式转换应该是合理的。在C++内建类型中,从int转换到double也是再合理不过的了(比从double转换到int更加合理)。看下面的例子:

     1 class Rational {
     2 
     3 public:
     4 
     5 Rational(int numerator = 0, // ctor is deliberately not explicit;
     6 
     7 int denominator = 1); // allows implicit int-to-Rational
     8 
     9 // conversions
    10 
    11 int numerator() const; // accessors for numerator and
    12 
    13 int denominator() const; // denominator — see Item 22
    14 
    15 private:
    16 
    17 ...
    18 
    19 };

    你想支持有理数的算术运算,比如加法,乘法等等,但是你不知道是通过成员函数还是非成员函数,或者非成员友元函数来实现。你的直觉会告诉你当你犹豫不决的时候,你应该使用面向对象的特性。有理数的乘积和有理数类相关,所有将有理数的operator*实现放在Rationl类中看上去是很自然的事。但违反直觉的是,Item 23已经论证过了将函数放在类中的方法有时候会违背面向对象法则,现在我们将其放到一边,研究一下将operator*实现为成员函数的做法:

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

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

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

    1 Rational oneEighth(1, 8);
    2 
    3 Rational oneHalf(1, 2);
    4 
    5 Rational result = oneHalf * oneEighth; // fine
    6 
    7 result = result * oneEighth; // fine

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

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

    1 result = oneHalf * 2; // fine
    2 
    3 result = 2 * oneHalf; // error!

    这就不太好了,乘法是支持交换律的。

    2. 问题出在哪里?

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

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

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

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

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

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

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

    1 const Rational temp(2); // create a temporary
    2 
    3 // Rational object from 2
    4 
    5 result = oneHalf * temp; // same as oneHalf.operator*(temp);

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

    1 result = oneHalf * 2; // error! (with explicit ctor);
    2 
    3 // can’t convert 2 to Rational
    4 
    5 result = 2 * oneHalf; // same error, same problem

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

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

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

    3. 解决方法是什么?

    然而你仍然希望支持混合模式的算术运行,但是方法现在可能比较明了了:使operator*成为一个非成员函数,这样就允许编译器在所有的参数上面执行隐式类型转换了:

     1 class Rational {
     2 
     3 ... // contains no operator*
     4 
     5 };
     6 
     7 const Rational operator*(const Rational& lhs, // now a non-member
     8 
     9 const Rational& rhs) // function
    10 
    11 {
    12 
    13 return Rational(lhs.numerator() * rhs.numerator(),
    14 
    15 lhs.denominator() * rhs.denominator());
    16 
    17 }
    18 
    19 Rational oneFourth(1, 4);
    20 
    21 Rational result;
    22 
    23 result = oneFourth * 2; // fine
    24 
    25 result = 2 * oneFourth; // hooray, it works!

    4. Operator*应该被实现为友元函数么?

    故事有了一个完美的结局,但是还有一个挥之不去的担心。Operator*应该被实现为Rational类的友元么?

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

    5. 其他问题

    如果你从面向对象C++转换到template C++,将Rational实现成一个类模版,会有新的问题需要考虑,并且有新的方法来解决它们。这些问题,方法和设计参考Item 46。

  • 相关阅读:
    144. Binary Tree Preorder Traversal
    excel 文本拼接
    excel中文转拼音(方便复制版本)
    odoo 日志文件太大处理,logfile自动轮替
    编码对象或者字串中包含Unicode字符怎样转换为中文
    odoo 返回成功提示信息
    odoo 对res_partner,res_users添加字段重启服务失败处理
    odoo 根据当前记录的值动态筛选many2many,many2one,one2many数据
    odoo 中%()d的使用
    nginx 监听非标准端口80,重定向端口丢失问题解决
  • 原文地址:https://www.cnblogs.com/harlanc/p/6482417.html
Copyright © 2020-2023  润新知