12. 在operator=中处理自赋值

前言

 
我们在重载operator=时需要注意自我赋值的情况。可能有人会认为这种情况很少发生,因为很少有人会写诸如w=w的句子。但实际上,代码中出现a[i]=a[j](i==j),或者p=q(指向同一对象)的例子屡见不鲜。
事实上,只要你使用了pointers或者references,而且它们被用来指向多个相同类型的对象,我们就需要考虑 是否会出现自赋值的情况。确切地说,只要有多个对象来自同一个继承体系,比如一个指针指向base class类型,而另一个指向derived class类型。它们完全可能指向的是同一个对象


不处理自赋值情况的风险

 
如果我们在class中手动地进行了资源管理,那么自赋值的处理不当可能会带来一些问题。举例而言:

1
2
3
4
5
6
7
8
9
10
11
12
class Bitmap{...}
class Widget{
...
private:
Bitmap* pb;
};
Widget&
Widget::operator=(const Widget& rhs){
delete pb;
pb = new Bitmap(*rhs.pb);//如果是自赋值,pb指向的对象已经释放
return *this;
}

上述程序既不具备异常安全性,也直接导致了“在停止使用资源前意外地释放了它”


解决方案

证同测试

其原理很简单:在函数最前端放上这样一句话:

1
if(this==&rhs) return *this;

这个解决方案保证了自赋值安全性,但是不具备异常安全性。在这句话中:
1
pb=new Bitmap(*rhs.pb);//已通过证同测试,此时已经执行delete

如果new导致了一个异常(内存不足或者copy构造函数抛出异常),那么pb始终会指向一块被删除的Bitmap。我们无法安全地删除pb,甚至读取它。

手动保证异常安全

因为只要解决了异常安全性,那么自赋值安全性就会必然得到解决,所以我们倾向于使用足够完美的逻辑避免异常的发生,其原理也很简单:在赋值pb前别删pb

1
2
3
4
bitnap * porig = pb;
pb =new bitmap(rhs.pb);
delete porig;
return *this;

这样的好处不言自明,如果pb没能正确赋值,其对象也没有被析构。(异常发生时跳出了operator=函数,因此没有执行delete porig)。如果正确赋值,pb指向的对象被释放。

copy&&swap

还有一个十分灵巧的operator=撰写技巧:

1
2
3
4
5
6
7
8
9
10
class Widget{
...
void swap(Widegt& rhs);//彻底交换数据
}
Widget&
Widget::operator=(const Widget& rhs){
Widget temp(rhs);
swap(temp);
return *this;
}

如果operator=接受的参数是by value而非by reference,那我们连temp都可以省略:

1
2
3
4
5
Widget&
Widget::operator=(Widget rhs){
swap(rhs);
return *this;
}

总结

  1. 确保自赋值安全性。其技术包括比较lhs与rhs的地址,先做backup,以及copy-and-swap
  2. 确定任何操作一个以上的对象的函数,如果其操作对象是同一个对象时,还能保持正确性。