“自我赋值”发生在对象被赋值给自己时:
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用来保存一个指针指向一块内存:
class Bitmap {...};
class Widget {
...
private:
Bitmap* pb;
};
下面是 operator=
实现代码,表面上看起来合理,但自我赋值出现时并不安全:
Widget& Widget::operator=(const Widget& rhs) {
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
这里的自我赋值问题是,operator=
函数内的 *this
和 rhs
有可能是同一对象。这样就会导致Widget持有一个指针指向一个已被删除的对象。
想要阻止这种错误,传统做法是使用if判断达到自我赋值检验的目的:
Widget& Widget::operator=(const Widget& rhs) {
if (this == &rhs) return *this; // protect against self-assignment
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
但是这两个版本都不具备异常安全性。如果 new Bitmap
导致异常,Widget则会指向一块被删除的Bitmap。你无法安全地删除他们,甚至无法安全地读取它们。
许多时候一些精心能安排的语句就可以实现异常安全(以及自我赋值安全)的代码了。例如以下代码,我们只需要注意在赋值pb所指对象之前不要删除pb:
Widget& Widget::operator=(const Widget& rhs) {
Bitmap* pOrig = pb;
pb = new Bitmap(*rhs.pb);
delete pOrig;
return *this;
}
现在,如果 new Bitmap
抛出异常,pb将保持原状。这段代码仍能处理自我赋值,因为我们对原对象做了一份副本、删除原Bitmap,然后指向新创建的副本。
在 operator=
函数内手动排列语句的一个替代方案是,使用copy and swap技术。这个技术由条款29详细说明。先看看其实现手法是什么样子:
class Widget {
...
void swap(Widget& rhs);
...
};
Widget& Widget::operator=(const Widget& rhs) {
Widget temp(rhs);
swap(temp);
return *this;
}
另一个方案利用以下事实:(1)某class的拷贝赋值运算符可能被声明为值传递;(2)值传递会造成一份副本。
Widget& Widget::operator=(const Widget& rhs) {
swap(rhs);
return *this;
}
这段代码效率更高但可读性较差。
请记住
- 确保当对象自我赋值时operator=有良好行为。其中技术包括比较源对象和目标对象的地址、精心周到的语句顺序、以及copy and swap。
- 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为仍然正确。