Item 11:赋值运算符的自赋值问题
自赋值是不安全的也是异常不安全的
当一个对象赋值给自己的时候就发生了一次自赋值。
class Widget { ... };
Widget w;
...
w = w; //@ 自赋值
这看起来很愚蠢,但它是合法的,所以应该确信客户会这样做。另外,赋值也并不总是那么容易辨别。例如,
a[i] = a[j]; //@ 如果i和j有同样的值,这里就是一次自赋值
*px = *py; //@ 如果px和py指向相同的东西,这里就是一次自赋值
如果两个对象来自同一个继承体系,甚至不需要公开声明,它们就是相同的类型,因为一个基类的引用或者指针也能够引向或者指向一个派生类的对象:
class Base { ... };
class Derived: public Base { ... };
void doSomething(const Base& rb, Derived* pd); //@ rb 和 pd 很可能是指向相同的对象
使用对象管理资源,并且应该确保那些资源管理对象被拷贝时行为良好。
如果你在试图管理资源,很大可能你会落入在你用完一个资源之前就已意外地将它释放的陷阱:
class Bitmap { ... };
class Widget {
...
private:
Bitmap *pb;
};
下面是一个表面上看似合理 operator= 的实现,但如果出现自赋值则是不安全的。它也不是异常安全的。
Widget& Widget::operator=(const Widget& rhs)
{
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
*this 和 rhs 可能是同一个对象。如果它们是,则那个 delete 不仅会销毁当前对象的位图,也会销毁 rhs 的 位图。在函数的结尾,Widget通过自赋值应该没有变化——发现自己持有一个指向已删除对象的指针。
防止这个错误的传统方法是在 operator= 的开始处通过一致性检测来阻止自赋值:
Widget& Widget::operator=(const Widget& rhs)
{
if (this == &rhs)
return *this;
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
这个版本能够解决自赋值安全,但是不能解决异常安全,例如:如果 "new Bitmap" 表达式引发一个异常(可能因为供分配的内存不足或者因为 Bitmap 的拷贝构造函数抛出一个异常),Widget 将以持有一个指向被删除的 Bitmap 的指针而告终。这样的指针是你不能安全地删除它们,你甚至不能安全地读取它们。
幸亏,使 operator= 异常安全一般也同时弥补了它的自赋值安全。这就导致了更加通用的处理自赋值问题的方法就是忽略它,而将焦点集中于达到异常安全。
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap *pOrig = pb;
pb = new Bitmap(*rhs.pb);
delete pOrig;
return *this;
}
如果 "new Bitmap" 抛出一个异常,pb 以及它所在的 Widget 的遗迹没有被改变。甚至不需要一致性检测,这里的代码也能处理自赋值,因为我们做了一个原始位图的拷贝,删除原始位图,然后指向我们作成的拷贝。这可能不是处理自赋值的最有效率的做法,但它能够工作。
另一个可选的手动排列 operator= 中语句顺序以确保实现是异常和自赋值安全的方法是使用被称为 "copy and swap" 的技术。
class Widget {
...
void swap(Widget& rhs);
...
};
Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs);
swap(temp);
return *this;
}
Summary
-
当一个对象被赋值给自己的时候,确保 operator= 是自赋值安全的和异常安全的。技巧包括:
- 比较源和目标对象的地址
- 忽略自赋值重点关注异常安全
- copy-and-swap