1.自我指派是如何發生的
當一個對象委派給自己的時候,自我指派就會發生:
1 class Widget { ... };
2
3 Widget w;
4
5 ...
6
7 w = w; // assignment to self、
這看上去是愚蠢的,但這是合法的,是以請放心,用戶端是可以這麼做的。此外,自身指派也并不總是很容易的能夠被辨識出來。舉個例子:
1 a[i] = a[j]; // potential assignment to self
上面的代碼在i和j相等的情況下就是自我指派,同樣的,看下面的例子:
*px = *py; // potential assignment to self
如果px和py恰巧指向同一個東西,那麼上面的語句就是自身指派。這些并不怎麼明顯的自我指派是使用别名的結果:也就是使用不止一種方法來指向同一個對象。一般情況下,當我們操作指向不同同類型對象的引用和指針時,需要考慮這些不同的對象是否是同一個對象。事實上,如果兩個對象來自同一個繼承體系,這兩個對象甚至不必聲明為同類型的,因為基類的指針或者引用可以指向派生類對象:
1 class Base { ... };
2
3 class Derived: public Base { ... };
4
5 void doSomething(const Base& rb, // rb and *pd might actually be
6
7 Derived* pd); // the same object
2.處理不好自我指派會使你掉入陷阱
如果你遵循Item13和Item14的建議,你就會使用對象來管理資源,并且你也能夠确信對資源進行管理的對象在進行拷貝時會運作的很好。在這種情況下,你的指派運算符有可能就是自我指派安全的,而不用去特定的考慮這件事情。如果你嘗試自己來管理資源(如果你自己寫一個資源管理類這是必須做的),你可能會掉入一個陷阱:在用完某個資源之前,資源突然被釋放掉了。舉個例子,假設你建立了一個類來管理一個原生指針,這個指針指向動态配置設定的bitmap對象:
1 class Bitmap { ... };
2
3 class Widget {
4
5 ...
6
7 private:
8
9 Bitmap *pb; // ptr to a heap-allocated object
10
11 };
下面是operator=的一個實作,從表面上看是合理的,但因為自我指派的存在,實際上它是不安全的。(它也不是異常安全的,我們稍會會處理)
1 Widget&
2
3 Widget::operator=(const Widget& rhs) // unsafe impl. of operator=
4
5 {
6
7 delete pb; // stop using current bitmap
8
9 pb = new Bitmap(*rhs.pb); // start using a copy of rhs’s bitmap
10
11 return *this; // see Item 10
12
13 }
自我指派的問題出現在operator=内部,*this(指派目标)和rhs可能是同一個對象。如果這是真的,delete不僅會為目前對象銷毀bitmap,也同樣會為ths銷毀bitmap。在函數的結尾,Widget對象本不應該通過自我指派有所改變,但你會發現現在它擁有的是一個指向被删除對象的指針!
3.處理自我指派的方法一:鑒定測試,防止自我指派
3.1 實作代碼
防止這個錯誤的傳統方法是在operator=函數的開始進行一個鑒定測試,看是否是一個自我指派:
1 Widget& Widget::operator=(const Widget& rhs)
2
3 {
4
5 if (this == &rhs) return *this; // identity test: if a self-assignment,
6
7 // do nothing
8
9 delete pb;
10
11 pb = new Bitmap(*rhs.pb);
12
13 return *this;
14
15 }
3.2這個方法的缺陷
這個方法是可以工作的,但是上面已經提到operator=的早先版本不僅是自我指派不安全的,同樣也是異常不安全的(exception-unsafe),在目前版本中關于異常的麻煩會繼續存在。特别的,如果”new Bitmap”語句産生一個異常(因為沒有足夠的記憶體可以配置設定或者因為Bitmap的拷貝構造函數抛出一個異常),Widget将會擁有一個指向被删除Bitmap對象的指針。這樣的指針是有毒的,因為你不能夠安全的釋放它們。你甚至不能夠安全的讀取它們。你唯一能夠做的安全的事情就是花費大量的調試的精力來找出問題出在哪裡。
4.處理自我指派的方法二:對語句進行排序
讓人高興的是,使operator=變得異常安全的方法也往往能使其變得自我指派安全。是以,我們将自我指派 的問題忽略掉,集中精力去達到異常安全。Item29比較深入的探索了異常安全,在這個條款中,我們隻需要觀察:對一些語句進行仔細的排序就可以生成exception安全(同樣能夠達到自我指派安全)的代碼,這就足夠了。舉個例子,我們隻需要注意在對pb指向對象的拷貝完成之前不要将pb釋放:
1 Widget& Widget::operator=(const Widget& rhs)
2
3 {
4
5 Bitmap *pOrig = pb; // remember original pb
6
7 pb = new Bitmap(*rhs.pb); // point pb to a copy of rhs’s bitmap
8
9 delete pOrig; // delete the original pb
10
11 return *this;
12
13 }
現在,如果”new BItmap”抛出異常,pb仍然不會發生變化。在沒有鑒别測試的情況下,這段代碼進行了自我指派,因為我們将源bitmap做了一份拷貝,讓pb去指向拷貝的資料,然後删除源bitmap。這也許不是處理自我指派的最有效率的方法,但這确實是可行的方法。
如果你關系效率,你可以将鑒别測試的代碼重新放回到函數的開始處。但是在這麼做之前,問問你自己,自我指派發生的頻率會有多高,因為鑒别測試不是免費的。它會增加一些代碼(obj檔案也會增大),同時引入了一個流程控制的分支,兩者都會使得程式運作速度變慢。Prefetching,caching和pipelining指令的效率都會降低。
5.處理自我指派的方法三:copy and swap
5.1 實作方法一
我們換一種方法來對operator=中的語句進行手動排序,來同時保證自我指派和異常安全,這種技術叫做拷貝和交換(copy and swap)。這種技術與異常安全是緊密相關的,是以會在Item29中描述。然而,它也是實作operator=的一個非常普通的方法,是以值得我們來看看這種實作方法究竟是什麼樣子:
1 class Widget {
2
3 ...
4
5 void swap(Widget& rhs); // exchange *this’s and rhs’s data;
6
7 ... // see Item 29 for details
8
9 };
10
11 Widget& Widget::operator=(const Widget& rhs)
12
13 {
14
15 Widget temp(rhs); // make a copy of rhs’s data
16
17 swap(temp); // swap *this’s data with the copy’s
18
19 return *this;
20
21 }
5.2 實作方法二
利用下面的兩個事實我們可以将上面的實作換一種寫法,這兩個事實是:(1)一個類的拷貝指派運算符可以被聲明為按值傳遞。(2)按值傳遞會對值進行拷貝。下面是另外一種寫法:
1 Widget& Widget::operator=(Widget rhs) // rhs is a copy of the object
2
3 { // passed in — note pass by val
4
5 swap(rhs); // swap *this’s data with
6
7 // the copy’s
8
9 return *this;
10
11 }
從個人觀點來說,我擔心這種方法為了聰明的實作而犧牲了代碼的清晰度,但是通過将拷貝操作從函數體内移動到函數的參數中,編譯器有時候能夠産生更高效的代碼,這是事實。
作者:
HarlanC部落格位址:
http://www.cnblogs.com/harlanc/個人部落格:
http://www.harlancn.me/本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出,
原文連結如果覺的部落客寫的可以,收到您的贊會是很大的動力,如果您覺的不好,您可以投反對票,但麻煩您留言寫下問題在哪裡,這樣才能共同進步。謝謝!