`

Effective c++条款23: 必须返回一个对象时不要试图返回一个引用

 
阅读更多

一旦程序员抓住了“传值”在效率上的把柄(参见条款22),他们会变得十分极端,恨不得挖出每一个隐藏在程序中的传值操作。岂不知,在他们不懈地追求纯粹的“传引用”的过程中,他们会不可避免地犯另一个严重的错误:传递一个并不存在的对象的引用。这就不是好事了。

看一个表示有理数的类,其中包含一个友元函数,用于两个有理数相乘:

class Rational {
public:
Rational(int numerator = 0, int denominator = 1);

...

private:
int n, d; // 分子和分母

friend
const Rational // 参见条款21:为什么
operator*(const Rational& lhs, // 返回值是const
const Rational& rhs)
};

inline const Rational operator*(const Rational& lhs,
const Rational& rhs)
{
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}

很明显,这个版本的operator*是通过传值返回对象结果,如果不去考虑对象构造和析构时的开销,你就是在逃避作为一个程序员的责任。另外一件很明显的事实是,除非确实有必要,否则谁都不愿意承担这样一个临时对象的开销。那么,问题就归结于:确实有必要吗?

答案是,如果能返回一个引用,当然就没有必要。但请记住,引用只是一个名字,一个其它某个已经存在的对象的名字。无论何时看到一个引用的声明,就要立即问自己:它的另一个名字是什么呢?因为它必然还有另外一个什么名字(见条款M1)。拿operator*来说,如果函数要返回一个引用,那它返回的必须是其它某个已经存在的Rational对象的引用,这个对象包含了两个对象相乘的结果。

但,期望在调用operator*之前有这样一个对象存在是没道理的。也就是说,如果有下面的代码:

Rational a(1, 2); // a = 1/2
Rational b(3, 5); // b = 3/5
Rational c = a * b; // c 为 3/10

期望已经存在一个值为3/10的有理数是不现实的。如果operator* 一定要返回这样一个数的引用,就必须自己创建这个数的对象。

一个函数只能有两种方法创建一个新对象:在堆栈里或在堆上。在堆栈里创建对象时伴随着一个局部变量的定义,采用这种方法,就要这样写operator*:

// 写此函数的第一个错误方法
inline const Rational& operator*(const Rational& lhs,
const Rational& rhs)
{
Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
return result;
}

这个方法应该被否决,因为我们的目标是避免构造函数被调用,但result必须要象其它对象一样被构造。另外,这个函数还有另外一个更严重的问题,它返回的是一个局部对象的引用,关于这个错误,条款31进行了深入的讨论。

那么,在堆上创建一个对象然后返回它的引用呢?基于堆的对象是通过使用new产生的,所以应该这样写operator*:

// 写此函数的第二个错误方法
inline const Rational& operator*(const Rational& lhs,
const Rational& rhs)
{
Rational *result =
new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
return *result;
}

首先,你还是得负担构造函数调用的开销,因为new分配的内存是通过调用一个适当的构造函数来初始化的(见条款5和M8)。另外,还有一个问题:谁将负责用delete来删除掉new生成的对象呢?

实际上,这绝对是一个内存泄漏。即使可以说服operator*的调用者去取函数返回值地址,然后用delete去删除它(绝对不可能——条款31展示了这样的代码会是什么样的),但一些复杂的表达式会产生没有名字的临时值,程序员是不可能得到的。例如:

Rational w, x, y, z;

w = x * y * z;

两个对operator*的调用都产生了没有名字的临时值,程序员无法看到,因而无法删除。(再次参见条款31)

也许,你会想你比一般的熊——或一般的程序员——要聪明;也许,你注意到在堆栈和堆上创建对象的方法避免不了对构造函数的调用;也许,你想起了我们最初的目标是为了避免这种对构造函数的调用;也许,你有个办法可以只用一个构造函数来搞掂一切;也许,你的眼前出现了这样一段代码:operator*返回一个“在函数内部定义的静态Rational对象”的引用:

// 写此函数的第三个错误方法
inline const Rational& operator*(const Rational& lhs,
const Rational& rhs)
{
static Rational result; // 将要作为引用返回的
// 静态对象

lhs和rhs 相乘,结果放进result;

return result;
}

这个方法看起来好象有戏,虽然在实际实现上面的伪代码时你会发现,不调用一个Rational构造函数是不可能给出result的正确值的,而避免这样的调用正是我们要谈论的主题。就算你实现了上面的伪代码,但,你再聪明也不能最终挽救这个不幸的设计。

想知道为什么,看看下面这段写得很合理的用户代码:

bool operator==(const Rational& lhs, // Rationals的operator==
const Rational& rhs); //

Rational a, b, c, d;

...

if ((a * b) == (c * d)) {

处理相等的情况;

} else {

处理不相等的情况;

}

看出来了吗?((a*b) == (c*d)) 会永远为true,不管a,b,c和d是什么值!

用等价的函数形式重写上面的相等判断语句就很容易明白发生这一可恶行为的原因了:

if (operator==(operator*(a, b), operator*(c, d)))

注意当operator==被调用时,总有两个operator*刚被调用,每个调用返回operator*内部的静态Rational对象的引用。于是,上面的语句实际上是请求operator==对“operator*内部的静态Rational对象的值”和“operator*内部的静态Rational对象的值”进行比较,这样的比较不相等才怪呢!

幸运的话,我以上的说明应该足以说服你:想“在象operator*这样的函数里返回一个引用”实际上是在浪费时间。但我没幼稚到会相信幸运总会光临自己。一些人——你们知道这些人是指谁——此刻会在想,“唔,上面那个方法,如果一个静态变量不够用,也许可以用一个静态数组……”

请就此打住!我们难道还没受够吗?

我不能让自己写一段示例代码来太高这个设计,因为即使只抱有上面这种想法都足以令人感到羞愧。首先,你必须选择一个n,指定数组的大小。如果n太小,就会没地方储存函数返回值,这和我们前面否定的那个“采用单个静态变量的设计”相比没有什么改进。如果n太大,就会降低程序的性能,因为函数第一次被调用时数组中每个对象都要被创建。这会带来n个构造函数和n个析构函数的开销,即使这个函数只被调用一次。如果说"optimization"(最优化)是指提高软件的性能的过程, 那么现在这种做法简直可以称为"pessimization"(最差化)。最后,想想怎么把需要的值放到数组的对象中以及需要多大的开销?在对象间传值的最直接的方法是通过赋值,但赋值的开销又有多大呢?一般来说,它相当于调用一个析构函数(摧毁旧值)再加上调用一个构造函数(拷贝新值)。但我们现在的目标正是为了避免构造和析构的开销啊!面对现实吧:这个方法也绝对不能选用。

所以,写一个必须返回一个新对象的函数的正确方法就是让这个函数返回一个新对象。对于Rational的operator*来说,这意味着要不就是下面的代码(就是最初看到的那段代码),要不就是本质上和它等价的代码:

inline const Rational operator*(const Rational& lhs,
const Rational& rhs)
{
return Rational(lhs.n * rhs.n, lhs.d * rhs.d);
}

的确,这会导致“operator*的返回值构造和析构时带来的开销”,但归根结底它只是用小的代价换来正确的程序运行行为而已。况且,你所担心的开销还有可能永远不会出现:和所有程序设计语言一样,C++允许编译器的设计者采用一些优化措施来提高所生成的代码的性能,所以,在有些场合,operator*的返回值会被安全地除去(见条款M20)。当编译器采用了这种优化时(当前大部分编译器这么做),程序和以前一样继续工作,只不过是运行速度比你预计的要快而已。

以上讨论可以归结为:当需要在返回引用和返回对象间做决定时,你的职责是选择可以完成正确功能的那个。至于怎么让这个选择所产生的代价尽可能的小,那是编译器的生产商去想的事.

请记住:
◆ 绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或
返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象.条款4已经为"在单线程环境中合理
返回reference指向一个local static对象"提供一份设计实例.

分享到:
评论

相关推荐

    Effective C++

    条款23:必须返回一个对象时不要试图返回一个引用 条款24:在函数重载与设定参数默认值间慎重选择 条款25:避免对指针与数字类型的重载 条款26:当心潜在的二义性 条款27:如果不想使用隐式生成的函数要显示的禁止它 条款...

    Effective.C++.中文第二版.50条款doc文档.chm

    条款23: 必须返回一个对象时不要试图返回一个引用 条款24: 在函数重载和设定参数缺省值间慎重选择 条款25: 避免对指针和数字类型重载 条款26: 当心潜在的二义性 条款27: 如果不想使用隐式生成的函数就要显式地禁止它...

    Effective C++ 中文版

    条款21:必须返回对象时,别妄想返回其reference 条款22: 将成员变量声明为private 条款23: 宁以non-member、non-friend替换member函数 条款24:若有所参数皆需类型转换,请为此采用non-member函数 条款25:考虑...

    Effective Modern C++:改善C++11和C++14的42个具体做法(中文版 + 英文版)

    《Effective Modern C++:改善C++11和C++14的42个具体做法(影印版)(英文版)》中包括以下主题:剖析花括号初始化、noexcept规范、完美转发、智能指针make函数的优缺点;讲解std∷move,std∷forward,rvalue引用和...

    Effective C++:改善程序与设计的55个具体做法(中文第三版)

    Effective C++:改善程序与设计的55个具体做法(中文第三版)亚马逊图书 放到Kindle上就可以浏览学习,因为亚马逊软件有防护功能,电脑上无法使用,切记!!!!

    More Effective C++

     More Effective C++一书充满了实用性高且掷地铿锵的忠告,为你每天可能面对的问题带来帮助。和其前一本兄弟书籍 Effective C++一样,More Effective C++对每一位以C++为开发工具的程序员而言,都必备读物。  继 ...

    Effective C++跟more Effective c++

    Effective C++跟more Effective c++

    C++: Effective Modern C++ (C++ 11, C++ 14)

    C++: Effective Modern C++ (C++ 11, C++ 14) (guide,C Programming, HTML, Javascript, Programming,all,internet, Coding, CSS, Java, PHP Vol 1) By 作者: Paul Laurence ISBN-10 书号: 1547133244 ISBN-13 书号:...

    effective c++和more effective c++

    电子版的effective c++ 和more effective c++ c++四书五经中介绍的经典书籍

    Effective C++ Digital Collection 140 Ways to Improve Your Programming (2018).rar

    学习C++ 和STL学习学习的书,三合一(共140种改善您的C++程序的经验之谈,C++程序员必读),英文原版,带书签,2018版本。难得的c++学习资料

    Effective C++(第三版)

    条款21:必须返回对象时,别妄想返回其reference don't try to return a reference when you must return an object. 条款22:将成员变量声明为private declare data members private. 条款23:宁以non-member、non-...

    Effective C++ More effective C++ 中文版 .chm .rar

    中文版 chm Effective C++ More effective C++

    effective c++ & more effective c++ 纯文字版

    effective c++ & more effective c++ 纯文字版

    Effective C++ & More Effective C++.chm

    Effective C++ & More Effective C++.chm

    Effective C++ 3个版本清晰pdf

    effective C++,第一版和第二版是中文pdf,第三版是英文的pdf

    Effective c++.pdf

    Effective c++.pdf Effective c++.pdf

    Effective C++(中文版).pdf

    《Effective C++:改善程序与设计的55个具体做法》(中文版)(第3版)一共组织55个准则,每一条准则描述一个编写出更好的C++的方式。每一个条款的背后都有具体范例支撑。第三版有一半以上的篇幅是崭新内容,包括讨论...

    effective c++ word版

    effective c++ word版 effective c++ word版 effective c++ word版 effective c++ word版 effective c++ word版 effective c++ word版 effective c++ word版 effective c++ word版 effective c++ word版 effective ...

    《Effective C++中文版:改善程序与设计的55个具体做法》第3版[PDF]

    当您读过《Effective C++中文版(第3版改善程序与设计的55个具体做法)》后,就获得了迅速提升自己C++功力的一个契机。  在国际上,本书所引起的反响,波及整个计算机技术出版领域,余音至今未绝。几乎在所有C++书籍...

Global site tag (gtag.js) - Google Analytics