C++ 处理好赋值运算符的自赋值 gaunthan Posted on Oct 19 2016 ? C++ ? > 对于包含有指针成员的类,我们常常需要自己实现赋值运算符。本文讲解实现赋值运算符的一些注意事项和要点。 <!--more--> ## 自赋值的陷阱 假设现有如下定义的类: ``` cpp class Bitmap { ... }; class Widget { ... private: Bitmap *pb; // 指向动态分配的对象 }; ``` 我们也许会在大意间为它实现了如下的赋值运算符: ``` cpp Widget& Widget::operator=(const Widget& rhs) { delete pb; // 先释放 pb = new Bitmap(*rhs.pb); // 后分配 return *this; } ``` 然而,当进行下面这样的调用时,程序崩溃了: Widget w; w = w; 理由很简单,在使用w.pb时,w.pb早已被销毁,这导致new语句中的Bitmap的构造函数引用了非法的内存。 ## 实现自赋值安全 避免上述问题的一个实现如下: ``` cpp Widget& Widget::operator=(const Widget& rhs) { if (this == &rhs) return *this; delete pb; pb = new Bitmap(*rhs.pb); return *this; } ``` 现在,我们可以执行 `w = w` 的操作了。 上述的实现其实还存在一个问题。假设在执行`pb = new Bitmap(*rhs.pb);`时,new表达式抛出了异常(内存不足或者是Bitmap的构造函数抛出了异常),则会导致pb成为了**悬挂指针**,它指向一个已经被销毁的Bitmap。为此,我们还需要考虑实现异常安全。 ## 实现自赋值异常安全 让人感到惊讶的是,实现了异常安全的赋值运算符版本,同时就实现了自赋值安全: ``` cpp Widget& Widget::operator=(const Widget& rhs) { Bitmap *pOrig = pb; // 暂存 pb = new Bitmap(*rhs.pb); // 先分配 delete pOrig; // 后销毁 return *this; } ``` 采用“暂存+先分配”的方案,使得即使new抛出了异常,pb指向的对象仍然能保持原样。 你也许想结合前面提到的 `if(this != &rhs) ...` 的方法来提高自赋值情况下的效率。但是如果大多数情况下都不是自赋值,则效率也许会大大打折。我们知道,if 语句是条件分支语句,它会导致执行流分支,从而可能导致分支预测惩罚,结果可能非常严重:指令预取错误,流水线断流、高速缓存失效等。这么说确实有点夸张了,如果大部分情况下都是自赋值的情况,并且你真的需要关心性能,就做一下测试,比较两种方案在你使用的编译环境中哪种更好。 在实际编程中,如果程序里经常需要执行自我赋值(这种操作在大多数程序中是很少的),并且对你的程序来说,通过对自我赋值进行检测能省去一些不必要的工作并因此极大地提高程序性能(这在大多数的程序中则更为少见),那么就应该对自我赋值进行检测。 > 可以将自我赋值检测作为一种优化手段,以避免不必要的工作,这是正确的做法。 ## 使用copy-swap实现 自赋值安全和异常安全的赋值运算符的一个更优雅的实现是使用copy和swap: ``` cpp class Widget { ... void swap(Widget& rhs); ... }; Widget& Widget::operator=(const Widget& rhs) { Widget temp(rhs); swap(temp); return *this; } ``` 如果你的类的赋值运算符是以值传递的,而不是const引用,则可以用以下的实现方案,这种方案在某些时候能够使编译器生成更高效的代码: ``` cpp Widget& Widget::operator=(Widget rhs) { swap(rhs); return *this; } ``` 关于 swap 函数的一些讨论见此文:[C++ 为自定义类型提供不抛出异常的 swap](http://leanote.com/blog/post/581d3e2bab644164d4011813)。 ## References - Meyers S. Effective C++[M]. 电子工业出版社, 2011. 赏 Wechat Pay Alipay C++ 尽可能推迟变量的定义 C++ STL 算法总结