讀書筆記 effective c++ Item 11 在operator=中處理自我賦值


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 }

從個人觀點來說,我擔心這種方法為了聰明的實現而犧牲了代碼的清晰度,但是通過將拷貝操作從函數體內移動到函數的參數中,編譯器有時候能夠產生更高效的代碼,這是事實。


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com