[轉]http://www.shnenglu.com/tiandejian/archive/2007/04/30/ecpp_11.html
第11條: 在 operator= 中要考慮到 自賦值問題
當對象對其自身賦值時,就發生了一次“自賦值”:
class Widget { ... };
Widget w;
...
w = w; // 自賦值
這樣做看上去沒什么意義,但這是合法的,因此以后我們假設客戶端程序員可能會這樣做。而且,賦值工作本身并不總是那么容易辨認的。比如:
如果 i 和 j 的值相同,那么這就是一次自賦值。另外
在 px 和 py 指向同一處時,上面一行也是一次自賦值。這些自賦值并不是那么一目了然,它們是由別名造成的:可以通過多種方式引用同一個對象。大體上講,用來操作指向同一類型多個對象的引用或指針的代碼都應考慮對象重復的問題。實際上,假如兩個對象來自同一層次,即使它們并未聲明為同一類型,也要考慮重復問題,這是因為一個基類的引用或指針可以引用或指向其派生類的類型的對象。
class Base { ... };
class Derived: public Base { ... };
void doSomething(const Base& rb, // rb 和 *pd 可能實際上
Derived* pd); // 是同一個對象
假設你遵循第 13 條和第 14 條中的建議,你將會一直使用對象來管理資源,而且在復制時你將會確保資源管理對象能正確工作。如果上邊的假設成立,你的賦值運算符很可能在處理自賦值時將是安全的,你不需要額外關注它。然而如果你試圖自己來管理資源(顯然你在編寫資源管理類時必須要這樣做),此時你很有可能陷入這個陷阱中:一個對象尚未用完,但是你卻不小心將其釋放了。比如說,你創建了一個類其中放置了一個無類型指針,你用這個指針來動態分配位圖:
class Bitmap { ... };
class Widget {
...
private:
Bitmap *pb; // 指向一個分配在堆上的對象
};
下邊給出 operator= 的一個實現,它在表面看上去很合理,但是如果存在自賦值,它便是不安全的。(它在出現異常時也不安全,稍后我們討論這個問題)
Widget&
Widget::operator=(const Widget& rhs) // operator= 不安全的實現
{
delete pb; // 停止使用當前的位圖
pb = new Bitmap(*rhs.pb); // 開始使用 rhs 位圖的一份拷貝
return *this; // 參見第 10 條
}
此處的自賦值問題出現在 operator= 的內部, *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;
}
這樣可以正常工作,但是我曾經說過 operator= 的早期版本不僅僅在賦值時不安全,在發生異常時它也會出現問題。特別地,如果“ new Bitmap ”語句引發了一個異常(有可能是可分配內存耗盡,或者是 Bitmap 的拷貝構造函數拋出了一個異常),最后 Widget 所包含的指針仍將指向一個已被刪除的 Bitmap 。這類指針是有毒的。你無法安全的刪除它們。你甚至沒辦法安全的讀取它們。此時你所做的唯一一件安全的事情也許就是耗費大量的精力去排查 bug 。
還好,在讓 operator= 在遇到異常時能安全執行的同時,它也不會在自賦值時出現問題了。因此,你可以把目光集中在異常的安全問題上,而忽略自賦值的問題。第 29 條中深入討論異常中的安全問題,但是本條中已經可以很清晰地看出:在許多情況下,認真安排一下語句可以使你的代碼在出現異常時是安全的(同時在自賦值時也是安全的)。比方說,現在我們只需要認真考慮:在我們沒有把 pb 對象復制出來以前,千萬不要刪除它:
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap *pOrig = pb; // 復制原始的 pb
pb = new Bitmap(*rhs.pb); // 讓 pb 指向 *pb 的這一副本
delete pOrig; // 刪除原始的 pb
return *this;
}
現在,如果“ new Bitmap ”拋出一個異常, pb (及其所在的 Widget )沒有被改動。即使沒有進行一致性檢測,這段代碼也可以解決自賦值問題,這是因為我們復制出了原始位圖的一個副本,并且刪除了原始副本,然后指向我們復制出的那個副本。這也許不是解決自賦值問題的最高效的途徑,但是這樣做確實有效。
如果你考慮到效率問題,你可以重新在程序最開端添加一致性檢測。然而在做這件事之前,問一下自己,你期望自賦值出現的有多頻繁,因為一致性檢測也有系統開銷。首先這使得代碼(源代碼和對象)變得稍長一些,同時它也會為控制流引入一個分支,這兩點都會降低運行的速度。比如說,指令預讀、捕獲、管線分配等操作的執行效率將會受到影響。
為了使 operator= 的實現對異常和自賦值都保證安全,必須為其手動安排語句,這里還有另一個途徑:使用一個稱為“復制并交換”的技術。這一技術更加貼近異常安全問題,所以我們在第 29 條中討論它。但是它在編寫 operator= 時得到了十分普遍的應用,看一下它實現的方法是十分值得的:
class Widget {
...
void swap(Widget& rhs); // 交換 *this 和 rhs 中的數據 ;
... // 更多細節請參見第 29 條
};
Widget& Widget::operator=(const Widget& rhs)
{
Widget temp(rhs); // 為 rhs 的數據保存副本
swap(temp); // 使用上邊的副本與 *this 交換
return *this;
}
上述的主題可以進行一下演變,可 以利用以下一些事實: (1) 一個類的拷貝賦值運算符的參數可以通過傳值方式實現; (2) 通過傳值可以傳遞這一參數的一個副本(參見第 20 條):
Widget& Widget::operator=(Widget rhs) // rhs is a copy of the object
{ // passed in — note pass by val
swap(rhs); // swap *this's data with
// the copy's
return *this;
}
從我個人角度來講,我很擔心這一點,這個手段會將清晰度作為“祭祀品”擺放在靈巧性的“祭壇”上,但是把復制操作從函數體中移出來,放在參數結構中,在一些場合確實能夠編寫出更加高效的代碼。
需要記住的
l 在一個對象為自己賦值時,要確保 operator= 可以正常地運行。可以使用的技術有:比較源對象和目標對象的地址、謹慎安排語句、以及“復制并交換”。
l 在兩個或兩個以上的對象完全一樣時,要確保對于這些重復對象的操作可以正常運行。