条款11:在operator= 中处理“自我赋值”
Handle assignment to self in operator=.
本章较长,分为两部分。
自我赋值
“自我赋值”发生在对象被赋值给自己时:
class Widget { ... };
Widget w;
...
w = w; //赋值给自己
虽然这种做法看起来比较傻,但是这种操作却是合法的。
此外,自我赋值并不是总是可以一眼分辨出来,例如:
a[i] = a[j]; //潜在的自我赋值
如果i和j具有相同的值时,这就是一个自我赋值。再比如:
*px = *py; //潜在的自我赋值
如果指针px和py恰巧指向同一个东西,这也是一个自我赋值。
这些并不明显的复制行为,是“别名(aliasing)”所带来的结果。所谓“别名”:
- 别名就是有一个以上的方法指称(指涉)某对象。
一般而言,如果某段代码操作pointers或references而它们被从来“指向多个相同类型的对象”,就需要去考虑这些对象是否为同一个对象。
实际上,两个对象只要来自同一个继承体系,它们甚至不需要声明为相同类型就可能会造成“别名”,因为一个base class的reference或者pointer可以指向一个derived class对象:
class Base { ... };
class Derived : public Base { ... };
void doSomethings(const Base& rb, Derived* pd); //rd和*pd有可能其实是同一个对象
在这里,假如说我们尝试自行管理资源(即打算写一个用于资源管理的class,就需要这样做),就可能会掉进“在停止使用资源之前意外释放了它”的陷阱。举个例子,假如建立一个class用来保存一个指针指向一块动态分配的位图(bitmap):
class Bitmap { ... };
class Widget {
...
private:
Bitmap* pb; //指针,指向一个从heap分配而得的对象
};
接着,下面的operator= 的实现代码,看起来虽然合理,但是在进行自我赋值时并不安全:
Widget& Widget::operator=(const Widget& rhs) //不安全的operator= 的实现版本
{
delete pb; //停止使用当前的bitmap
pb = new Bitmap(*rhs.pb); //使用rhs's bitmap的副本(复件)
return *this;
}
之所以会出现自我赋值的问题,operator= 函数内的*this(赋值的目的端)和rhs有可能是同一个对象。如果它们是同一个对象,那么delete对象就不只是销毁当前对象的bitmap,它也同时销毁了rhs的bitmap。
因此,在函数末尾,对于Widget,它原本不应该被自我赋值动作改变的,然而此时:
- Widget发现自己持有一个指针,指向一个已经被删除的对象。
想要阻止这样的错误,传统的做法是:
- operator= 最前面进行一个“证同测试(identity test)”,以此达到自我赋值的检验目的:
Widget& Widget::operator=(const Widget& rhs)
{
if(this == &rhs) //证同测试(identity test)
return *this; //如果是自我赋值,就不做任何事情
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
这种解决办法是行得通的。在第一个版本的operator= 中,不仅不具备“自我赋值安全性”,也不具备“异常安全性”,然而,这个新版本的operator=,仍然存在异常方面的问题:
如果“new BItmap”导致了异常(比如因为分配时内存不足或者因为Bitmap的copy构造函数抛出异常),Widget最终会持有一个指针指向一个被删除的Bitmap。这样的指针是有害的:
- 既不能安全的删除它,也不能安全的读取它。
唯一能对它们做的安全的事情就是付出很多调试的功夫,去找到错误的起源。