条款21:必须返回对象时,别妄想返回其reference

1.前言

一旦程员意识到pass-by-value的效率问题,往往就会陷入一种误区,一心一意根除pass-by-value带来的种种问题。在坚定追求pass-by-reference的路上,肯定会进入一个误区,即:开始传递一些references指向不存在的对象。这可不是件好事。

2.举例说明

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

class Rational{

    public:
        Rational(int numerator=0,int denominator=1);
    ....
    private:
        int n,d;
        const Rational operator*(const Rational& lhs,const Rational& rhs);
};

 这个版本的operator* 是以by value方式返回其计算结果(一个对象),如果改而传递reference,就不需要考虑构造函数和析构函数带来的开销。但是所谓reference只是个名称,代表某个即有对象。

任何时候看到一个reference声明式,都应该立刻问自己,它的别名是啥。因为它一定是某对象的另一个名称,如果它返回一个reference,后者一定指向某个既有的Rational对象,内含两个Rational对象的乘积。

首先,我们不能期望这样一个Rational对象在调用operator*之前就存在,也就是说,如果你有:

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

期望“原本就存在一个其值为3/10的Rational对象”并不合理。如果operator*要返回一个reference指向这样一个数值,它必须自己创建那个Rational对象。

3.函数创建新对象的途径

函数创建新对象的途径有二:在stack空间或者heap空间创建。如果定义一个local变量,就是在stack空间创建。根据这个策略试写operator*如下:

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对象在函数退出之前已经被销毁了。因此这个版本的operayor*并未返回reference指向某个Rational,它返回的reference已经被销毁了。

因为事情的真相就是任何函数如果返回一个reference指向某个local对象,都将一败涂地。

假设让我们考虑在heap内构造一个对象,并返回reference指向它。Heap_based对象由new创建,所以你得写一个heap-base operator* 如下:

const Rational& operator*(const Rational& lhs,const Rational& rhs)
{
    Rational* result=new Rational(lhs.n*rhs.n,lhs.d*rhs.d)l//警告,更糟糕的写法
    return *result;
}

这里还是必须付出一个"构造函数调用"代价,因为分配所得到的内存将以一个适当的构造函数完成初始化。但现在面临着另外一个问题:谁该对这被你New出来的对象实施delete?

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

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 Ratioanl对象”:

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

}

就像所有用上static对象设计的一样,这一个也同样造成我们对多线程安全性的疑虑。不过这还只是比较浅显的弱点,如果想看更深层次的瑕疵,参考以下代码:

bool operator==(const Rational& lhs,const Rational& rhjs);//针对Rational写的operator=
Rational a,b,c,d;
....
if((a*b)==(c*d))
{
    当乘积相等,相关操作;
    
}else
{
    当乘积不等,做相应的动作;
}

程序运行的结果显示((a*b)==(c*d))总是被核算为true,不论a,b,c和d的值是什么.

这里需要注意:在operator==被调用之前,已经有两个operator*调用式起作用,每一个都返回reference指向operator*内部定义的static Ratioanl对象。因此operator==被要求将“operator*内的static Rational对象值”拿来和“operator*内的static Rational对象值做比较”,如果结果值不相等才奇怪。(两次operator*调用的确改变了static Rational对象值,但由于它们返回的是reference,因此调用端看到的永远是static Rational对象值的”现值“)

一个”必须返回新对象“函数的正确写法是:就让那个函数返回一个新对象。对Rational的operator*而言,意味着以下写法:

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

当然,我们需要承受operator*返回值的构造成本和析构成本,然而长远来看,那只是为了获得正确行为而付出的小小代价

  • 20
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值