EffectiveCpp-21:必须返回对象时,不要返回reference
Effective C++ Item 21:必须返回对象时,不要返回reference
众所周知,C++中函数传参pass-by-value的效率是要低于pass-by-reference的,所以函数传参尽量以pass-by-reference-to-const 替换 pass-by-value,但是在函数返回的时候,返回一个reference并不一定是一件好事,因为这可能会导致我们传递一些reference并不存在的对象。
考虑一个用于表现有理数的class,内含一个函数用来计算两个有理数的乘积:
1 |
|
这个类的operator*
是以by-value返回其计算结果。如果现在你想节省掉该对象的构造和析构函数成本,而改用传递reference,那么请先回想一下,所谓reference只是一个名称,代表某个既有对象。任何时候看到一个reference对象声明式,都要立刻提醒自己,它的另一个名称是什么?因为它一定是某物的另一个名称。如果上面
operator*
返回reference,那么它一定指向一个既有的Rational
对象,内含两个Rational
对象的乘积。
1 |
|
以上面的代码为例,期望一个值为3/10
的Rational对象已经存在并不合理,如果operator*
返回一个reference指向如此数值,它必须自己创建那个Rational对象。
创建新对象的方式有两种:在stack空间或者在heap空间创建。如果要定义一个local变量,就是在stack空间上创建对象。
1 |
|
然后这样做有一个很明显的漏洞:函数返回一个reference指向result,但result是一个local对象,而local对象在函数退出之前就已经销毁了。因此这个版本的operator*
并未返回reference指向某个Rational,它返回的额reference指向一个已经被销毁的“从前的”Rational。而任何使用到这个返回值的操作都会引发“无定义行为”的报错。所以,任何函数都不要返回reference指向一个local对象。
那么考虑在heap内构建对象,并返回reference指向它,Heap-based对象由new创建,所以写一个heap-based
operator*
如下:
1 |
|
即便如此,我们还是要付出一个“构造函数”的代价,因为分配所得的内存将以一个适当的构造函数并完成初始化操作。但此外你又有了另一个问题:谁该对着这个new出来的对象实施delete?
即便调用者诚实诚谨,并出于良好意识,他们还是不太能在这样合情合理的用法下阻止内存泄漏:
1 |
|
这里同一个语句调用了两次operator*
,也就需要使用两次new,对应的就需要两次delete。但是却没有合理的方法让operator*
的使用者进行那些delete调用,因为没有合理的方法让他们取得operator*
返回的reference背后隐藏的那个指针。这一定会导致内存泄露。
所以不管是on-the-stack或者是on-the-heap的做法,都会因为operator*
的返回结果调用构造函数而出错或付出代价。而我们想返回引用的最初目的是避免构造函数的调用。
或许还有一种避免任何构造函数被调用的方法,那就是“让operator*
返回的reference指向一个被定义于函数内部的static
Rational对象”:
1 |
|
然而这也是一个非常糟糕的设计,看下面这段代码:
1 |
|
不管a
,b
,c
,d
的值是什么,表达式(a * b) == (c * d)
一定为true
。上述的if
判别式可以写为下面的等价形式:
1 |
|
在operator==
被调用之前,两个operator*
已经被调用,每个都返回reference指向operator*
内部定义的static对象。因此operator==
比较的两个对象都是operator*
内定义的static对象,所以判别式一定是true
。
总结
绝不要返回pointer或reference指向一个local stack对象,或返回有一个reference指向一个heap-allocated对象,或返回pointer或reference指向一个loacl staic对象而又可能同时需要多个这样的对象。