洛译小筑

别来无恙,我的老友…
随笔 - 45, 文章 - 0, 评论 - 172, 引用 - 0
数据加载中……

[ECPP读书笔记 条目24] 当函数所有的参数需要进行类型转换时,要将其声明为非成员函数

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

class Rational {

public:

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

                                   // 构造函数是有意声明为非显性的,

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

 

  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引用”类型的参数,请参见条目32021

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

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);         // 出错!

但是在本示例中,没有任何非成员的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;              // 同样的错误,同样的问题

虽然上述的两条语句无法支持混合模式算术,但是至少二者的行为依旧保持一致。

然而你的目标是:即能保持一致性,又能支持混合模式算术,换句话说,寻找出让上述两条语句均能通过编译的设计方案。让我们返回先前的两条语句,来讨论一下:为什么即使Rational的构造函数不是explicit的,二者依然是一条可通过编译,另一条则通不过:

result = oneHalf * 2;                // 工作正常 (oneHalf拥有非显性构造函数)

 

result = 2 * oneHalf;                // 错误! (即使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指针所指的参数),那么它必须是一个非成员函数。

posted on 2007-07-05 23:23 ★ROY★ 阅读(1215) 评论(1)  编辑 收藏 引用 所属分类: Effective C++

评论

# re: 【翻译】[Effective C++中文版第3版][第24条]当函数所有的参数需要进行类型转换时,要将其声明为非成员函数  回复  更多评论   

与成员函数所调用的对象(也就是 this 所指向的对象)相关的隐式参数永远也没有资格进行隐式转换
**********************************************
这个是必然的, 因为编译器根本无法确定你要转到什么类型。


2007-07-06 08:43 | SmartPtr

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