Widget 中,有一個 Bitmap 型指針 pb
class Bitmap; class Widget { private: Bitmap *pb; // ptr to a heap-allocated object };
1 重載 “op=”
在 Widget 類中重載 "=" 時,需考慮以下方面
1.1 鏈式賦值
整數 15 首先賦值給 z,得到新值的 z 再賦值給 y,接着得到新值的 y 最后再賦值給 x,如下所示:
int x, y, z; x = y = z = 15; // chain of assignments
相當於
x = (y = (z = 15));
為了實現鏈式賦值,函數的返回值須是一個實例自身的引用,也即 *this; 同理,重載其它的復合賦值運算符 (如 +=, -=, *=, /=),也必須在函數結束前返回 *this
Widget& Widget::operator=(const Widget& rhs) { delete pb; // stop using current bitmap pb = new Bitmap(*rhs.pb); // start using a copy of rhs's bitmap return *this; }
1.2 自賦值
其次要考慮的是,關於自賦值的情況,雖然顯式的自賦值並不常見,但潛在的隱式自賦值仍需注意
Widget w; ... w = w; // explict assignment to self a[i] = a[j]; // potential assignment to self *px = *py; // potential assignment to self
解決方法是,在函數內加一個 if 語句,判斷當前實例 (*this) 和傳入的參數 rhs 是不是同一個實例,也即判斷是不是自賦值的情況
如果是自賦值,則不作任何處理,直接返回 *this;如果不是自賦值,首先釋放實例自身已有內存,然后再分配新的內存,如下所示:
Widget& Widget::operator=(cosnt Widget& rhs) { if (this == &rhs) return *this; // identity test: if a self-assignment, do nothing delete pb; pb = new Bitmap(*rhs.pb); return *this; }
1.3 異常安全
上例中,假如在分配內存時,因內存不足或 Bitmap 的拷貝構造函數異常,導致 "new Bitmap" 產生異常 (exception),則 pb 指向的是一個已經被刪除的 Bitmap
考慮異常安全,一個方法是先用 new 分配新內容,再用 delete 釋放如下代碼的內容,如下所示:當 "new Bitmap" 拋出一個異常時,pb 指針並不會改變
Widget& Widget::operator=(cosnt Widget& rhs) {
if (this == &rhs) return *this; // identity test
Bitmap *pOrig = pb; // remember original pb pb = new Bitmap(*rhs.pb); // 注意:"." 的優先級高於 "*" delete pOrig; // delete the original pb return *this; }
如果不考慮效率的問題,那么即使沒有對自賦值進行判斷的 if 語句,其后面的語句也足以應付自賦值的問題
2 拷貝-交換
上例中,因為效率的問題,保留了 if 語句,但實際上,因為自賦值出現的概率很低,所以上述代碼看似“高效”,其實並不然
最常用的兼顧自賦值和異常安全 (exception safety) 的方法是 “拷貝-交換” (copy-and-swap),如下所示:
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; }
2.1 std::swap
std::swap 屬於標准算法,其實現如下:
namespace std
{ template<typename T> // typical implementation of std::swap void swap(T& a, T& b) // swaps a's and b's values { T temp(a); a = b; b = temp; } }
以上有三個拷貝:首先拷貝 a 給 temp,然后拷貝 b 給 a,最后拷貝 temp 給 b
2.2 Widget::swap
對於 Widget 類,實現兩個 Widget 對象的值交換,只需互換 Bitmap *pb 即可,這稱為 pimpl (pointer to implementation)
首先,定義一個 swap 公有成員函數,如下:
void Widget::swap(Widget& other) { using std::swap; swap(pb, other.pb); // to swap Widgets, swap their pb pointers }
然后,模板特例化 std::swap 函數,調用上面的 swap 函數,實現指針互換
namespace std { template<> // revised specialization of std::swap void swap<Widget>(Widget& a, Widget& b) { a.swap(b); // to swap Widgets, call their swap member function } }
3 智能指針
綜上所述,重載賦值操作符,需要考慮鏈式賦值、自賦值和異常安全,頗為繁瑣
一個簡化方法是,在 Widget 類中聲明一個智能指針
class Widget { ... private: std::unique_ptr<Bitmap> pBitmap; // smart pointer };
此時,重載 "op=",則只需考慮鏈式賦值
Widget& Widget::operator=(const Widget& rhs) // copy operator= { *pBitmap = *rhs.pBitmap; // "." 的優先級高於 "*"
return *this; }
理論上應該可行,尚未在實際項目中驗證 (留待后續測試...)
小結:
1) 重載類賦值操作符,首先考慮鏈式賦值 -- 函數返回 *this,其次考慮自賦值和異常安全 -- “拷貝-交換”
2) 考慮寫一個不拋異常的 swap 函數 (consider support for a non-throwing swap)
3) 被重載的類賦值操作符 "op=" 必須定義為成員函數,其它的復合賦值操作符 (如 "+=", "-=" 等) 應該被定義為成員函數
4) 類中使用智能指針,可大大簡化重載賦值操作符 “op=” 的實現
參考資料:
<Effective C++_3rd> item 10, 11, 25
<劍指 offer> 2.2.1
<Effective Modern C++> item 22