Effective C++ Item 11: 在operator=中处理自赋值问题
一、定义赋值运算符函数需要注意的问题:
- 返回值类型声明为引用,并在函数返回前返回*this。因为只有返回引用,才能允许连续赋值。
- 传入参数应声明为常量引用,否则从形参到实参会多调用一次拷贝构造函数,降低代码效率。赋值运算符函数不会改变传入的实例的状态,因此传入参数应添加const关键字。
- 是否释放实例自身已有的内存。如果忘记分配新内存之前,释放自身已有的空间,程序将出现内存泄露。
- 是否判断传入的参数和this是不是同一个实例。如果是同一个实例,则直接返回this。如果事先不判断就进行赋值,那么在释放实例自身的内存的时候就会导致严重的问题:当*this和传入的参数是同一个实例时,那么一旦释放了自身的内存,传入的参数的内存也同时被释放了,因此再也找不到需要赋值的内容了。
from 剑指offer
二、什么是自我赋值?
“自我赋值”发生在对象被赋值给自己时:
class Widget { ... };
Widget w;
...
w = w; //赋值给自己
虽然这种做法看起来比较傻,但是这种操作却是合法的。
此外,自我赋值并不是总是可以一眼分辨出来,例如:
- 如果i和j具有相同的值时,这就是一个自我赋值。
a[i] = a[j]; //潜在的自我赋值
- 如果指针px和py恰巧指向同一个东西,这也是一个自我赋值。
*px = *py; //潜在的自我赋值
这些并不明显的复制行为,是“别名(aliasing)”所带来的结果。 比如,当基类指针和派生类指针指向同一个基类的对象时,自我赋值就可能发生。
三、不安全的做法
举个例子,假如建立一个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;
}
*this和rhs可能是相同的对象,这样的话delete不仅删除this指针的bitmap,也删除了rhs的bitmap。
问题:不仅自赋值不安全,异常处理也不安全,因为new Bitmap操作是可能产生异常的。
四、解决方法
1. 传统做法——Identity test, 自赋值检测
Widget& Widget::operator=(const Widget& rhs)
{
if (this == &rhs) return *this; // identity test: if a self-assignment,
// do nothing
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
缺点:new Bitmap的异常安全问题仍存在。
2. 复制pb所指东西之前不要删除pb
实现异常安全通常也能实现自赋值安全。
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap *pOrig = pb; // remember original pb
pb = new Bitmap(*rhs.pb); // make pb point to a copy of *pb
delete pOrig; // delete the original pb
return *this;
}
- 现在如果new Bitmap抛出异常,pb保持不变。即使不做identity test,它也能处理好自赋值问题。
- 相对来说,这种方法的效率较低。如果考虑效率问题,可以函数顶部添加自赋值检测代码。但这样做之前,需要考虑自赋值操作发生的频率可能有多高,因为这样做会有一定开销,因为它会导致代码增加以及引入了流程控制分支,会降低运行速度。比如,指令预取、缓存和流水线的效率可能被降低。
3. copy and swap
class Widget {
...
void swap(Widget& rhs); // exchange *this's and rhs's data;
... // see Item 29 for details
};
Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs); // make a copy of rhs's data
swap(temp); // swap *this's data with the copy's
return *this;
}
五、如果类成员含有的unique_ptr指针,而没有raw pointer,那么在它的赋值运算符函数中不处理自赋值问题会有什么问题吗?
如果类成员只有std::unique_ptr,而没有裸指针,那么在赋值运算符函数中不处理自赋值问题是没有问题的。
原因如下:
std::unique_ptr的赋值操作会自动处理自赋值问题。当一个std::unique_ptr被赋值时,原有的内存将被自动删除,然后才会接管新的内存。
举例来说,假设有以下的代码:
class Foo {
public:
std::unique_ptr<int> ptr;
Foo& operator=(const Foo& other) {
if (this != &other) {
ptr = std::make_unique<int>(*other.ptr);
}
return *this;
}
};
即使不进行自赋值检查,当进行自赋值时,std::unique_ptr的赋值操作也不会导致问题。这是因为std::unique_ptr的赋值操作会首先删除当前的内存,然后再分配新的内存。因此,即使是自赋值,也不会有问题。
然而,这不意味着在所有的情况下都可以忽略自赋值检查。如果类成员是裸指针或者其他需要手动管理内存的资源,那么就必须在赋值运算符函数中处理自赋值问题,以防止内存泄露或者其他问题。