C++条款 必须返回对象时,别妄想返回其reference 12/55

必须返回对象时,别妄想返回其reference

Don't try to return a reference when you musst return an object

一旦程序员领悟了pass-by-value的效率牵连层面,往往变成十字军战士,一心一意根除pass-by-value带来的种种邪恶。在坚定追求pass-by-reference的纯度中,他们一定会犯下一个致命错误:开始传递一些references指向其实并不存在的对象。

考虑一个用以表现有理数的class,内含一个函数用来计算两个有理数的乘积:

class Rational{
public:
    Rational(int numerator = 0, int denominator = 1);
    ...
private:
    int n, d;        //分子n和分母d
    friend
        const Rational
            opeartor* (const Rational& lhs, const Rational& rhs);
}

这个版本的operator* 系以by value方式返回其计算结果。但需要承担该返回对象的构造和析构成本。若非必要,没有人会想要为这样的对象付出太多代价,问题是可以不付出任何代价吗?

如果可以改而传递reference,就不需付出代价。但是reference只是个对称,代表某个已经存在的对象。任何时候看到一个reference声明式,你都应该立刻问自己,它的另一个名称是什么?因为它一定是某物的另一个名称。例如

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

但原本并没有存在一个其值为3/10的对象能传给c。如果operator* 要返回一个reference指向如此数值,它必须自己创建那个Rational对象。

函数创建新对象的途径有二:在stack空间或在heap空间创建之。如果定义一个local变量,就是在stack空间创建对象:

const Rational& operator* (const Rational& lhs, const Rational & rhs)
{
    Rational result(lhs.n * rhs.n *, lhs.d * rhs.d);    //糟糕的代码
    return result;
}

我们一开始的目标就是要避免调用构造函数,而result却必须像任何对象一样地由构造函数构造起来。更严重的是:这个函数返回一个reference指向result,但result是个local对像,而local对象在函数退出前就被销毁了。因此它返回的reference指向一个旧时的Rational;一个曾经被当做Rational但现今已经成空、发臭、败坏的残骸。任何调用者甚至只是对此函数的返回值做任何一点点的运用,都将立刻坠入“无定义行为”的恶地。

于是,让我们考虑在heap内构造一个对象,并返回reference指向它。Heap-based对象有new创建,所以你得写一个heap-based operator* 如下:

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?

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

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

同一个语句内调用了两次opeartor*,因而需要使用两次delete。但却没有合理的办法让operator*使用者进行那些delete调用,因为没有合理的办法让他们取得operator*返回的reference背后隐藏的那个对象。这绝对导致资源的泄漏。

或许你已经注意到了,上述不论on-the-stack或on-the-heap做法,都因为operator*返回的结果调用函数而受惩罚。而我们最初的目标是要避免如此的构造函数调用动作。或许你认为你还能另辟蹊径来避免构造函数被调用,此法奠基于“让operator*返回的reference指向的一个被定义于函数内部的static Rational对象”:

const Rational& operator* (const Rational& lhs, const Rational& rhs)
{
    static Rational result;    //static对象,此函数返回其reference
    result = ...;
    return resulr;            //将lhs乘以rhs,并将结果置于result内
}

就像所有用上static对象的设计一样,这一个也立刻造成我们对多线程安全性的疑虑。不过那还只是它显而易见的弱点。如果想看看更深层的瑕疵,考虑以下面这些完全合理的客户代码:
 

bool operator = (const Rational& lhs, 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*调用式起作用,每一个都返回reference指向operator*内部定义的static Rational对象。因此operator==被要求将“operator*内的static对象值”拿来和“operator*内的static对象值”比较,如果比较结果不相等,那才奇怪呢

现在应该足够令人信服,欲令诸如operator*这样的函数返回reference,只是浪费时间而已。

一个“必须返回新对象”的函数正确写法是:就让那个函数返回一个新对象(pass-by-value):

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

当然,你需要承受operator*返回值的构造成本和析构成本,然而长远来看那只是为了获得正确行为而付出的一个小小的代价。但万一对象太大,承受不起,别忘了C++和所有编程语言一样,允许编译器实现者施行最优化,用以改善产出码的效率却不改变其可观察的行为。因此某些情况下operator*返回值的构造和析构可被安全地消除。如果编译器运用这一事实,你的程序将继续保持它们该有的行为,而执行起来又比预期快。

总结:

  • 绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象。

​​​​​​​编于04/06/2019 14:40

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值