Item 11: 在 operator= 中處理 assignment to self(自指派)
作者:Scott Meyers
譯者:fatalerror99 (iTePub's Nirvana)
釋出:http://blog.csdn.net/fatalerror99/
當一個 object(對象)指派給自己的時候就發生了一次 assignment to self(自指派):
class Widget { ... };
Widget w;
...
w = w; // assignment to self
這看起來很愚蠢,但它是合法的,是以應該确信客戶會這樣做。另外,assignment(指派)也并不總是那麼容易辨識。例如,
a[i] = a[j]; // potential assignment to self
如果 i 和 j 有同樣的值就是一個 assignment to self(自指派),還有
*px = *py; // potential assignment to self
如果 px 和 py 碰巧指向同一個東西,這也是一個 assignment to self(自指派)。這些不太明顯的 assignments to self(自指派)是由 aliasing(别名)(有不止一個方法引用一個 object(對象))造成的。通常,使用 references(引用)或者 pointers(指針)操作相同類型的多個 objects(對象)的代碼需要考慮那些 objects(對象)可能相同的情況。實際上,如果兩個 objects(對象)來自同一個 hierarchy(繼承體系),甚至不需要聲明為相同的類型,因為一個 base class(基類)的 reference(引用)或者 pointer(指針)也能夠引向或者指向一個 derived class(派生類)類型的 object(對象):
class Base { ... };
class Derived: public Base { ... };
void doSomething(const Base& rb, // rb and *pd might actually be
Derived* pd); // the same object
如果你遵循 Items 13 和 14 的建議,你應該總是使用 objects(對象)來管理 resources(資源),而且你應該確定那些 resource-managing objects(資源管理對象)被拷貝時行為良好。在這種情況下,你的 assignment operators(指派運算符)在你沒有考慮自指派的時候可能也是 self-assignment-safe(自指派安全)的。然而,如果你試圖自己管理 resources(資源)(如果你正在寫一個 resource-managing class(資源管理類),你當然必須這樣做),你可能會落入在你用完一個 resource(資源)之前就已意外地将它釋放的陷阱。例如,假設你建立了一個 class(類),它持有一個指向動态配置設定 bitmap(位圖)的 raw pointer(裸指針):
class Bitmap { ... };
class Widget {
...
private:
Bitmap *pb; // ptr to a heap-allocated object
};
下面是一個表面上看似合理 operator= 的實作,但如果出現 assignment to self(自指派)則是不安全的。(它也不是 exception-safe(異常安全)的,但我們要過一會兒才會涉及到它。)
Widget&
Widget::operator=(const Widget& rhs) // unsafe impl. of operator=
{
delete pb; // stop using current bitmap
pb = new Bitmap(*rhs.pb); // start using a copy of rhs's bitmap
return *this; // see Item 10
}
這裡的 self-assignment(自指派)問題在 operator= 的内部,*this(指派的目标)和 rhs 可能是同一個 object(對象)。如果它們是,則那個 delete 不僅會銷毀 current object(目前對象)的 bitmap(位圖),也會銷毀 rhs 的 bitmap(位圖)。在函數的結尾,Widget ——通過 assignment to self(自指派)應該沒有變化——發現自己持有一個指向已删除 object(對象)的指針!
防止這個錯誤的傳統方法是在 operator= 的開始處通過 identity test(一緻性檢測)來阻止 assignment to self(自指派):
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;
}
這個也能工作,但是我在前面提及那個 operator= 的早先版本不僅僅是 self-assignment-unsafe(自指派不安全)的,它也是 exception-unsafe(異常不安全)的,而且這個版本還有異常上的麻煩。詳細地說,如果 "new Bitmap" 表達式引發一個 exception(異常)(可能因為供配置設定的記憶體不足或者因為 Bitmap 的 copy constructor(拷貝構造函數)抛出一個異常),Widget 将以持有一個指向被删除的 Bitmap 的指針而告終。這樣的指針是有毒的,你不能安全地删除它們。你甚至不能安全地讀取它們。你對它們唯一能做的安全的事情大概就是花費大量的調試精力來斷定它們起因于哪裡。
幸虧,使 operator= exception-safe(異常安全)一般也同時彌補了它的 self-assignment-safe(自指派安全)。這就導緻了更加通用的處理 self-assignment(自指派)問題的方法就是忽略它,而将焦點集中于達到 exception safety(異常安全)。Item 29 更加深入地探讨了 exception safety(異常安全),但是在本 Item 中,已經足以看出,在很多情況下,仔細地調整一下語句的順序就可以得到 exception-safe(異常安全)(同時也是 self-assignment-safe(自指派安全))的代碼。例如,在這裡,我們隻要注意不要删除 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" 抛出一個 exception(異常),pb(以及它所在的 Widget)的遺迹沒有被改變。甚至不需要 identity test(一緻性檢測),這裡的代碼也能處理 assignment to self(自指派),因為我們做了一個原始 bitmap(位圖)的拷貝,删除原始 bitmap(位圖),然後指向我們作成的拷貝。這可能不是處理 self-assignment(自指派)的最有效率的做法,但它能夠工作。
如果你關心效率,你可以在函數開始處恢複 identity test(一緻性檢測)。然而,在這樣做之前,先問一下自己,你認為 self-assignments(自指派)發生的頻率是多少,因為這個檢測不是免費午餐。它将使代碼(源代碼和目标代碼)有少量增大,而且它将在控制流中引入一個分支,這兩點都會降低運作速度。例如,instruction prefetching(指令預讀),caching(緩存)和 pipelining(流水線操作)的效力都将被降低。
另一個可選的手動排列 operator= 中語句順序以確定實作是 exception- and self-assignment-safe(異常和自指派安全)的方法是使用被稱為 "copy and swap" 的技術。這一技術和 exception safety(異常安全)關系密切,是以将在 Item 29 中描述。然而,這是一個寫 operator= 的足夠通用的方法,值得一看,這樣一個實作看起來通常就像下面這樣:
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;
}
在這個主題上的一個變種利用了如下事實:(1)一個 clsaa(類)的 copy assignment(拷貝指派運算符)可以被聲明為 take its argument by value(以傳值方式取得它的參數);(2)通過傳值方式傳遞某些東西以做出它的一個 copy(拷貝)(參見 Item 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;
}
對我個人來說,我擔心這個方法在靈活的祭壇上犧牲了清晰度,但是通過将拷貝操作從函數體中轉移到參數的構造中,有時能使編譯器産生更有效率的代碼倒也是事實。
Things to Remember
- 當一個 object(對象)被指派給自己的時候,確定 operator= 是行為良好的。技巧包括比較 source(源)和 target objects(目标對象)的位址,關注語句順序,和 copy-and-swap。
- 如果兩個或更多 objects(對象)相同,確定任何操作多于一個 object(對象)的函數行為正确。