Shuffy

不断的学习,不断的思考,才能不断的进步.Let's do better together!
posts - 102, comments - 43, trackbacks - 0, articles - 19

[转]http://www.cppblog.com/tiandejian/archive/2007/07/05/ec_24.html
我在这本书的序言中曾特别提到过,让类支持隐式类型转换在一般情况下都不会是一个好主意。当然,这一准则还是存在一些例外的,其中最普通的一个就是数值类型。举例说,如果你正在设计一个表示有理数的类,提供从整数向有理数的转换也不是毫无道理的。很显然,这样做与 C++ 内建的从 int 向 double 的转换 一样符合常理(甚至比 C++ 内建的从 double 向 int 的转换 要合理得多)。这是千真万确的,你可能以这样的方式开始编写你的 Rational (有理数) 类:

class Rational {

public:

 Rational(int numerator = 0, int denominator = 1);

                              // 构造函数是有意写成非显性的

                              // 从而可以提供 int Rational 的隐性转换

 

 int numerator() const;      // 用于访问分子和分母的函数

 int denominator() const;    // 参见第 22

 

private:

 ...

};

此时你很了解这个类应该支持诸如加法、乘法等算术操作,但是你并不能确定这些操作是应该通过成员函数实现,或者(如果可能的话)以非成员函数(友元)的形式实现。在你举棋不定的时候,你的本能会告诉你你应该尽量做到面向对象。你知道这一点,于是会说,有理数的乘法操作 Rational 类相关,因此很自然地,有理数的 operator* 就应该实现为 Rational 类内部的成员。与直觉恰恰相反的是,将函数放在相关的类中在有些时候恰恰是违背面向对象原则的(第 23 条中讨论过),我们暂时不考虑这一问题,考察一下用 operator* 作为 Rational 的一个成员函数:

class Rational {

public:

 ...

 

 const Rational operator*(const Rational& rhs) const;

};

(如果你不太了解为什么以这种方式定义函数:返回一个 const 值而不是引用,使用“ const 引用”类型的参数。请参见第 3 20 21 条)

这种设计方案会使乘法操作非常简便:

Rational oneEighth(1, 8);

Rational oneHalf(1, 2);

 

Rational result = oneHalf * oneEighth;        // 工作正常

 

result = result * oneEighth;                  // 工作正常

但是你不能满足于现状。你可能期望 Rational 支持混合模式操作,也就是说 Rational 应该可以与其它类型值(比如 int )相乘。毕竟说,两数相乘的操作再自然不过了,即使这两个数的类型不一致。

然而,当你尝试进行混合模式算术时,你会发现它仅仅在一半的时间内正常工作:

result = oneHalf * 2;                        // 工作正常

 

result = 2 * oneHalf;                        // 出错!

这是一个不好的兆头。你是否记得乘法交换率呢?

如果你将上述后两个示例重写为它们等价的函数形式,代码中的问题就会浮出水面:

result = oneHalf.operator*(2);               // 工作正常

 

result = 2.operator*(oneHalf);               // 出错!

oneHalf 对象是一个类的实例,这个类中包含 operator* ,于是编译器就会调用这个函数。然而整数 2 没有相关的类,因此就没有相关的 operator* 成员函数。编译器仍然会去寻找非成员函数 operator* (应该存在于名字空间域或者整体域),这些 operator* 应该可以这样调用:

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

但是在本示例中,没有任何非成员 operator* 能接收一个 int 和一个 Rational ,因此搜寻工作自然会失败。

请再次关注一下调用成功的示例。你可以看到它的第二个实在参数是整数 2 ,而 Rational::operator* 本身只将 Rational 作为它的型参。这里发生了什么呢? 2 为什么仅在一种情况下正常运行,而另一种又不可以了呢?

这里发生的事情是:隐式类型转换。编译器知道你正在传入一个 int ,而函数所需要的参数却是 Rational ,但是编译器同时也知道它可以通过使用你所提供的 int 值作为参数,调用 Rational 的构造函数,从而“变出”一个合适的 Rational 来。也就是说,编译器在处理上述代码时,会以近似于下面的形式进行:

const Rational temp(2);          // 2 为参数,创建一个

                                // 临时的 Rational 对象

 

result = oneHalf * temp;        // oneHalf.operator*(temp) 等价

当然,编译器这样做仅仅是因为有一个非显性的构造函数为其助一臂之力。如果 Rational 的构造函数是 explicit 的,那么下面的语句都是通不过编译的:

result = oneHalf * 2;           // 出错 ! ( 存在 explicit 的构造函数 )

                                // 无法将 2 转型为 Rational

 

result = 2 * oneHalf;           // 同样的错误,同样的问题

看上去似乎仅在这些参数存在于参数表中的时候,它们才有资格进行隐式类型转换。与成员函数所调用的对象(也就是 this 所指向的对象)相关的隐式参数永远也没有资格进行隐式转换。这就是为什么第一次调用能够通过编译,而第二次不行。第一种情况涉及到参数表中所列的一个参数,而第二种没有。

但是此时你仍期望支持混合模式算术,同时在此时工作方案也水落石出了:将 operator* 声明为非成员函数,这样就可以允许编译器对所有参数进行隐式类型转换:

class Rational {

 ...                            // 不包含任何 operator*

};

 

const Rational operator*(const Rational& lhs, const Rational& rhs)

                                // operator* 声明为非成员函数

{

 return Rational(lhs.numerator() * rhs.numerator(),

                  lhs.denominator() * rhs.denominator());

}

 

Rational oneFourth(1, 4);

Rational result;

 

result = oneFourth * 2;         // 工作正常

 

result = 2 * oneFourth;         // 太棒了!这样也可以了。

故事终于有了一个完美的结局,但是还为人们留下了一处悬念。 operator* 是否应该做为 Rational 类的一个友元呢?

在这种情况下,答案是:不行。因为 operator* 完全可以通过 Rational 的公用接口来实现。上面的代码交待了如何做这件事情。我们可以从中观察总结出一条重要结论,那就是:与成员函数相反的是非成员函数,而不是友元函数。有太多的 C++ 程序员自认为,如果一个函数与一个类相关,那么就不应该将其实现为成员(比如说,所有参数都需要进行类型转换),而是应该实现为一个友元。这个实例表明这样的推理是存在漏洞的。要尽量避免使用友元,因为,与现实生活中的情况类似,朋友为我们带来的麻烦往往要比好处多得多。当然就像歌里唱的:“朋友多了路好走”,但是这并不意味着一个函数不应该作为成员时,就必须成为一个友元。

本条款中包含着真理,仅仅包含真理,而又不是真理的全部。当你从面向对象的 C++ 过渡至模板 C++ 时(参见第 1 条),你会将 Rational 实现为模板类而不是普通的类,此时就需要考虑新的问题了,也有了新的解决办法,一些设计实现的方法是不可思议的。这些问题、解决方案、具体实现是第 46 条讨论的主题。

铭记在心

如果你需要对一个函数的所有参数进行类型转换(包括 this 指针所指向的对象),那么它必须是一个非成员函数。


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理