Don’t try to return a reference when you must return an object
考虑:开始传递一些 references 指向其实并不存在的对象,这可不是好事。
e,g,
下面写一个带有两有理数(rational numbers)相乘功能的 class,
class Rational {
public:
Rational(int numerator = 0, int denominator = 1);
...
private:
int a, b;
friend const Rational operator* (const Rational& l, const Rational& r);
};
如果可以改为传递 reference,就不需要付出代价。但是记住,所谓 reference 只是一个名称,代表某个既有的对象。任何时候看到一个 reference 声明式,你都应该立刻问自己,它代表的是哪个对象?因为它一定是某物的另一个名称。以上述 operator* 为例,如果它返回一个 reference,后者一定指向某个既有的 Rational 对象,内含两个 Rational 对象的乘积。
我们当然不可能期望这样一个(内含乘积的)Rational 对象在调用 operator* 之前就存在。也就是说,如果有:
Rational a(2, 1); // a = 2/1
Rational b(3, 4); // b = 3/4
Rational c = a * b; //c 应该是 3/2
期望 “ 原本就存在一个值为 3/2 的 Rational 对象 ” 并不合理。如果 operator* 要返回一个 reference 指向如此数值,它必须自己创建那个 Rational 对象。
函数创建新对象的途径有二:在 stack 空间或在 heap 空间创建之。如果定义一个 local 变量,就是在 stack 空间创建对象。根据这个策略写下 operator* 如下:
const Rational& operator* (const Rational& l, const Rational& r){
Rational result(l.a * r.a, r.a * r.b); // 警告!糟糕的代码!
return result;
}
你可以拒绝这种做法,因为你的目标是要避免调用构造函数,而 result 却必须像任何对象一样由构造函数构造出来。更严重的是:
这个函数返回一个 reference 指向 result,但 result 是个 local 对象,而 local 对象在函数退出前就被销毁了。
真相是,任何函数如果返回一个 reference 指向某个 local 对象,都将一败涂地。(如果函数返回指针指向一个 local 对象,也是一样)
于是,我们考虑在 heap 内构造一个对象,并返回 reference 指向它。Heap-based 对象由 new 创建,所以你得写一个 heap-based operator* 如下:
const Rational& operator* (const Rational& l, const Rational& r){
Rational* result = new Rational(l.a * l.b, r.a * r.b); // 警告!更糟糕的写法!
return *result;
}
唔,你还是必须得付出一个 “ 构造函数调用 ” 的代价,因为分配所得到的内存将以一个适当的构造函数完成初始化操作。但此外你现在又有了另一个问题:谁该对着被你 new 出来的对象实施 delete?
即使调用者诚实谨慎,并且出于良好意识,他们还是不太能够在这样合情合理的用法下阻止内存泄露:
Rational w, x, y, z;
w = x * y * z; // 与 operator*(operator*(x, y), z) 相同
这里,同一个语句内调用了两次 operator*,因而两次使用 new,也就需要两次 delete。但却没有合理的办法让 operator* 使用者进行那些 delete 的调用,因为没有合理的办法让他们取得 operator* 返回的references 背后隐藏的那个指针。这绝对导致资源泄露。
或许你已经注意到了,上述不论 on-the-stack 或 on-the-heap 做法,都因为对 operator* 返回的结果调用构造函数而受到惩罚。
或许你想到了另一种方式,让 operator* 返回的 reference 指向一个被定义于函数内部的 static Rational 对象。但这会立刻造成我们对多线程安全性的疑虑。不多赘述,反正这样做很不好。
建议:一个 “ 必须返回新对象 ” 的函数的正确写法是:就让那个函数返回一个新对象呗。
对 Rational 的 operator* 而言意味以下写法:
inline const Rational operator* (const Rational& l, const Rational& r){
return Rational(l.a * l.b, r.a * r.b);
}
当然,你需要承受 operator* 返回值的构造成本和析构成本,然而长远来看那只是为了获得正确行为而付出的一个小小代价。
总结:当你必须在 “ 返回一个 reference 和返回一个 object ” 之间抉择时,你的工作就是挑出行为正确的那个。
请记住:
绝不要返回 pointer 或 reference 指向一个 local stack 对象,或返回 reference 指向一个 heap-allocated 对象,或返回 pointer 或 reference 指向一个 local static 对象而有可能同时需要多个这样的对象。