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
2
3
4
5
6
7
8
class Rational{
public:
Rational(int numerator = 0, int denominator = 1);

private:
int n, d; // 分子numerator 和 分母denominator
friend const Rational operator*(const Rational &lhs, const Rational &rhs);
};

这个类的operator*是以by-value返回其计算结果。如果现在你想节省掉该对象的构造和析构函数成本,而改用传递reference,那么请先回想一下,所谓reference只是一个名称,代表某个既有对象。任何时候看到一个reference对象声明式,都要立刻提醒自己,它的另一个名称是什么?因为它一定是某物的另一个名称。如果上面 operator*返回reference,那么它一定指向一个既有的Rational对象,内含两个Rational对象的乘积。

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

以上面的代码为例,期望一个值为3/10的Rational对象已经存在并不合理,如果operator*返回一个reference指向如此数值,它必须自己创建那个Rational对象。

创建新对象的方式有两种:在stack空间或者在heap空间创建。如果要定义一个local变量,就是在stack空间上创建对象。

1
2
3
4
5
const Rational& operator* (const Rational &lhs, const Rational &rhs)
{
Rational result(lhs.n * rhs.n, lhs.d * rhs.d);
return result;
}

然后这样做有一个很明显的漏洞:函数返回一个reference指向result,但result是一个local对象,而local对象在函数退出之前就已经销毁了。因此这个版本的operator*并未返回reference指向某个Rational,它返回的额reference指向一个已经被销毁的“从前的”Rational。而任何使用到这个返回值的操作都会引发“无定义行为”的报错。所以,任何函数都不要返回reference指向一个local对象

那么考虑在heap内构建对象,并返回reference指向它,Heap-based对象由new创建,所以写一个heap-based operator*如下:

1
2
3
4
5
const Rational* operator* (const Rational &lhs, const Rational &rhs)
{
Rational *result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
return *result;
}

即便如此,我们还是要付出一个“构造函数”的代价,因为分配所得的内存将以一个适当的构造函数并完成初始化操作。但此外你又有了另一个问题:谁该对着这个new出来的对象实施delete?

即便调用者诚实诚谨,并出于良好意识,他们还是不太能在这样合情合理的用法下阻止内存泄漏:

1
2
Rational w, x, y, z;
w = x * y * z; // 与 operator* (operator* (x, y), z)相同

这里同一个语句调用了两次operator*,也就需要使用两次new,对应的就需要两次delete。但是却没有合理的方法让operator*的使用者进行那些delete调用,因为没有合理的方法让他们取得operator*返回的reference背后隐藏的那个指针。这一定会导致内存泄露。

所以不管是on-the-stack或者是on-the-heap的做法,都会因为operator*的返回结果调用构造函数而出错或付出代价。而我们想返回引用的最初目的是避免构造函数的调用

或许还有一种避免任何构造函数被调用的方法,那就是“让operator*返回的reference指向一个被定义于函数内部的static Rational对象”:

1
2
3
4
5
6
7
8
const Rational& operator* (const Rational &lhs, const Rational &rhs)
{
static Rational result;

result = ...;

return result;
}

然而这也是一个非常糟糕的设计,看下面这段代码:

1
2
3
4
5
6
7
8
bool operator* (const Rational &lhs, const Rational &rhs);

Rational a, b, c, d;

if((a * b) == (c * d)) // 这个表达式一定为true
...
else
...

不管a,b,c,d的值是什么,表达式(a * b) == (c * d)一定为true。上述的if判别式可以写为下面的等价形式:

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

operator==被调用之前,两个operator*已经被调用,每个都返回reference指向operator*内部定义的static对象。因此operator==比较的两个对象都是operator*内定义的static对象,所以判别式一定是true

总结

绝不要返回pointer或reference指向一个local stack对象,或返回有一个reference指向一个heap-allocated对象,或返回pointer或reference指向一个loacl staic对象而又可能同时需要多个这样的对象。


EffectiveCpp-21:必须返回对象时,不要返回reference
https://gstarmin.github.io/2023/04/20/EffectiveCpp-21/
作者
Starmin
发布于
2023年4月20日
许可协议