讀書筆記 effective c++ Item 29 為異常安全的代碼而努力。本站提示廣大學習愛好者:(讀書筆記 effective c++ Item 29 為異常安全的代碼而努力)文章只能為提供參考,不一定能成為您想要的結果。以下是讀書筆記 effective c++ Item 29 為異常安全的代碼而努力正文
異常安全在某種意義上來說就像懷孕。。。但是稍微想一想。在沒有求婚之前我們不能真正的討論生殖問題。
假設我們有一個表示GUI菜單的類,這個GUI菜單有背景圖片。這個類將被使用在多線程環境中,所以需要mutex進行並發控制。
1 class PrettyMenu { 2 public: 3 ... 4 void changeBackground(std::istream& imgSrc); // change background 5 ... // image 6 7 private: 8 Mutex mutex; // mutex for this object 9 10 Image *bgImage; // current background image 11 12 13 14 int imageChanges; // # of times image has been changed 15 16 };
我們看一種PrettyMenu的changeBackground函數的可能實現:
1 void PrettyMenu::changeBackground(std::istream& imgSrc) 2 { 3 4 lock(&mutex); // acquire mutex (as in Item 14) 5 6 delete bgImage; // get rid of old background 7 8 9 ++imageChanges; // update image change count 10 bgImage = new Image(imgSrc); // install new background 11 12 unlock(&mutex); // release mutex 13 14 }
1. 異常安全的函數有什麼特征
從異常安全的角度來說,這個函數很糟糕。對於異常安全來說有兩個要求,上面的實現沒有滿足任何一個。
當異常被拋出時,異常安全的函數:
處理資源洩露問題很容易,因為Item 13解釋過了如何使用對象來管理資源,Item 14引入了Lock類來確保mutex能夠被實時的釋放掉:
1 void PrettyMenu::changeBackground(std::istream& imgSrc) 2 { 3 Lock ml(&mutex); // from Item 14: acquire mutex and 4 // ensure its later release 5 delete bgImage; 6 ++imageChanges; 7 bgImage = new Image(imgSrc); 8 }
使用像Lock一樣的資源管理類的一個極大的好處是它通常使函數更短小。看一下為什麼不再需要對unlock的調用了?作為一個通用的規則,代碼越少越好,因為對代碼做改動時,出錯和理解錯誤的可能性變低了。
2. 異常安全的三種保證級別
接下來讓我們看一看數據結構被損壞的問題。這裡我們要做出選擇,但是在我們可以進行選擇之前,必須對定義這些選擇的術語做一下比較。
異常安全的函數提供了如下三種保證的一種:
使用提供強保證的函數比只提供基本保證的函數要更加容易,因為調用提供強保證的函數之後,只可能有兩種程序狀態:函數被正確執行後的狀態,或者函數被調用之前的狀態。而如果在調用只提供基本保證的函數的時候拋出異常,程序可以進入任何有效狀態。
認為帶有空異常明細(empty exception specification)的函數是無異常的,這可能看上去是合理的,但事實上不是這樣。舉個例子,考慮下面的函數:
1 int doSomething() throw(); // note empty exception spec.
這並不是說doSomething永遠不會拋出異常。它的意思是如果soSomething拋出異常,就會是一個嚴重的錯誤,並且會調用意料不到的函數。事實上,doSomething沒有提供任何異常安全保證。這個函數的聲明(如果有異常明細,也包含異常明細)並沒有告訴你這個函數是否是正確的,可移植的或者效率高的,也沒有為你提供任何異常安全保證。所有這些特性都由函數的實現來決定,而不是聲明。
異常安全的代碼必須提供上面三種保證的一種。如果沒有提供,它就不是異常安全的。你的選擇決定了為你所實現的函數提供哪種保證。除了在處理異常不安全的舊代碼時不需要提供異常安全保證之外,異常不安全的代碼只有在下面一種情況下才會需要:你的團隊做需求分析時發現有對資源洩露和在破環的數據結構上運行程序的需要。
作為普通標准,提供最強異常安全保證是實際的想法。從異常安全的角度來說,不拋出異常的函數才是完美的。但在C++的C部分中很難不去調用可能會拋出異常的函數。使用動態分配內存的任何東西(例如,所有的STL容器)如果發現沒有足夠的內存可供分配都會拋出一個bad_alloc異常(Item 49)。如果能提供不拋出異常的函數更好,更多的情況是在基本保證和強保證之間做出選擇。
3. 提供異常安全的兩種方法 3.1 使用智能指針
對於changeBackground來說,提供強保證不是多難的事。首先,我們將PrettyMenu的bgImage數據成員的類型從內建的Image*指針替換為一種資源管理智能指針(見 Item 13)。說真的,對於防止資源洩露來說這絕對是一個好方法。它幫我們提供強異常安全保證的事實只是簡單對Item 13中的論述(使用對象管理資源是好的設計的基礎)做了進一步的加強。在下面的代碼中,我將會展示tr1::shared_ptr的使用,因為當進行拷貝時使用tr1::shared_ptr比使用auto_ptr更加直觀,因此更受歡迎。
其次,我們對changeBackground中的語句進行重新排序,達到只有image被修改的時候才會增加imageChnages的目的。作為通用准則,一個對象的狀態沒有被修改就表明一些事情沒有發生。
下面是最終的代碼:
1 class PrettyMenu { 2 ... 3 std::tr1::shared_ptr<Image> bgImage; 4 ... 5 }; 6 void PrettyMenu::changeBackground(std::istream& imgSrc) 7 { 8 Lock ml(&mutex); 9 bgImage.reset(new Image(imgSrc)); // replace bgImage’s internal 10 // pointer with the result of the 11 // “new Image” expression 12 ++imageChanges; 13 }
注意這裡不再需要手動delete舊image,因為這由智能指針在內部處理。並且,銷毀操作只有在新image成功創建的時候才會發生。更精確的說,只有在參數(new Image(imgSrc)的結果)被成功創建的時候tr1::shared_ptr::reset函數才會被調用。Delete只在reset函數內部被使用,所以如果reset不被調用,delete永遠不會被執行。注意資源管理對象的使用再次削減了changeBackground的長度。
正如我所說的,上面的兩個修改足以為changeBackground提供強異常安全保證。還有美中不足的就是關於參數imgSrc。如果Image的構造函數拋出異常,輸入流的讀標記可能會被移動,這個移動致使狀態發生變化並且對程序接下來的運行是可見的。如果changeBackground不處理這個問題,它只能提供基本異常安全保證。
3.2 拷貝和交換
把上面的問題放到一邊,我們假設changeBackground能夠提供強異常安全保證。(你應該能想出一個好的辦法來提供強異常安全保證,也許可以將參數類型從istream變為包含image數據的文件的名字。)有一種普通的設計策略也能提供強保證,熟悉它很重要。這個策略叫做“拷貝和交換”(copy and swap)。它是很簡單的:先對你想要修改的對象做一份拷貝,然後在拷貝上進行所有需要的改動。如果任何修改操作拋出了異常,源對象仍然保持未修改狀態。一旦修改完全成功,將源對象和修改後的對象進行不會拋出異常的交換即可(Item 25)。
這往往會把真實的對象數據放入到一個單獨的實現對象中,然後提供一個指向這個實現對象的指針。也即是指向實現的指針(pimpl idiom),Item 31中會進行詳細描述。PrerttyMenu的實現如下:
1 struct PMImpl { // PMImpl = “PrettyMenu 2 std::tr1::shared_ptr<Image> bgImage; // Impl.”; see below for 3 int imageChanges; // why it’s a struct 4 }; 5 class PrettyMenu { 6 ... 7 private: 8 Mutex mutex; 9 std::tr1::shared_ptr<PMImpl> pImpl; 10 }; 11 void PrettyMenu::changeBackground(std::istream& imgSrc) 12 { 13 using std::swap; // see Item 25 14 Lock ml(&mutex); // acquire the mutex 15 16 std::tr1::shared_ptr<PMImpl> // copy obj. data 17 18 pNew(new PMImpl(*pImpl)); 19 20 21 pNew->bgImage.reset(new Image(imgSrc)); // modify the copy 22 ++pNew->imageChanges; 23 24 swap(pImpl, pNew); // swap the new 25 // data into place 26 27 } // release the mutex
在這個例子中,我選擇將PMImpl定義為一個結構體而不是類,因為PrettyMenu的封裝性通過pImpl的私有性(private)來保證。把PMImpl定義成類會至少和結構體一樣好,雖然有一些不方便。如果需要,PMImpl可以被放在PrettyMenu中,但是打包問題(packaging)是我們所關心的。
拷貝和交換策略是處理對象狀態問題的卓越方法(狀態要麼全變要麼都不變),但是一般情況下,它不能夠保證所有的函數是強異常安全的。想知道為什麼,考慮對changeBackground做的一個抽象,someFunc,這個函數使用拷貝和交換策略,但也包含對其它兩個函數的調用,f1和f2:
1 void someFunc() 2 { 3 4 ... // make copy of local state 5 6 f1(); 7 8 f2(); 9 10 11 12 ... // swap modified state into place 13 14 }
這裡如果f1或者f2不是強異常安全的,someFunc就很難是強異常安全的。舉個例子,假設f1只提供了基本保證。如果someFunc要提供強保證,必須寫代碼在調用f1之前確定整個程序的狀態,然後捕獲f1中的所有異常,如果發生異常則恢復原始狀態。
即使f1和f2是強異常安全的,情況也沒有任何改觀。因為如果f1運行完成後,程序的狀態發生了變化,這時候如果f2拋出了異常,程序的狀態和調用someFunc之前已經不一樣了,即使f2沒有修改任何東西。
4. 不能提供強異常安全保證的兩種情況
當函數操作只影響本地狀態(例如,someFunc只影響調用此函數的對象的狀態),提供強異常安全保證是相對容易的。當函數對非本地數據也產生副作用時,提供強保證就相當困難了。例如,如果調用f1的副作用是數據庫會被修改,很難讓someFunc提供強異常安全。通常來說,對於已經提交的數據庫改動,沒有方法對其進行回退。其它的數據庫客戶端可能已經看到了數據庫的新狀態。
即使你想提供強異常安全保證,上述問題也會阻止你。另外一個問題是效率問題。拷貝和交換的關鍵點在於首先對對象拷貝進行修改,然後將源數據和修改後的數據進行無異常的交換。這需要對每個要修改的對象都做一份拷貝,這需要時間和空間,你可能不能夠或不願意為其提供這些資源。大家都想獲得強異常安全保證,你應該在實際的時候提供它,但不是100%的情況下都是實際可行的。
5. 至少為代碼提供基本異常安全保證(遺留代碼除外)如果不切實際,你必須提供基本保證。在實際情況中,你可能發現你可以為一些函數提供強保證,但是在效率和復雜度方面的開銷使其變得不再實際。只要你為提供強異常安全保證的函數做出努力了,沒有人會因為你提供基本保證而批評你。對許多函數來說,基本保證是最合理的選擇。
如果你實現一個函數不提供任何異常安全的保證,事情就不一樣了,因為你會一直內疚下去直到證明你是無辜的。所以你應該實現異常安全的代碼。但是你可能有所抵觸。再考慮一下someFunc的實現,它調用了函數f1和函數f2。假設f2沒有提供異常安全保證,連基本保證也沒有提供。這就意味著如果在f2內部拋出異常,資源洩露就可能會發生,也可能會出現被破壞的數據結構,例如,有序的數組不再有序,從一個數據結構傳遞到另一個數據結構的對象被丟失等等。someFunc沒有任何方法能夠對這些問題做出補償。如果函數someFunc調用了沒有提供異常安全的函數,someFunc自己也不能提供任何保證。
讓我們回到懷孕的話題。一個女性要麼懷孕了要麼沒有懷孕。不可能部分的懷孕把。類似的,一個軟件系統要麼是異常安全的,要麼不是。也沒有部分異常安全的系統。如果一個系統中有一個沒有提供異常安全的函數,那麼整個系統也就不是異常安全的,因為對這個函數的調用會導致資源洩露和數據結構的破壞。不幸的是,許多C++遺留代碼並沒有被實現為異常安全的,所以如今太多的系統都不是異常安全的。
沒有任何理由維持這種狀態。所以當寫新代碼或修改現有代碼時,對如何使其變得異常安全需要進行仔細的考慮。首先使用對象管理資源(Item 13),這能防止資源洩露。然後為每個函數從三種異常安全保證中選取實際並且最強的那一個,只有在調用遺留代碼時讓你無可選擇的情況下才能勉強接受無安全保證。為函數的使用者和將來的維護人員將你做的決定記錄在文檔中。一個函數異常安全保證是接口的可見部分,所以在你選擇異常安全保證部分時,你應該像選擇函數接口的其它方面一樣謹慎。
40年前,goto語句被認為是好的實踐。現在我們卻努力實現結構化控制流(structured control flows)。20年前,全局訪問數據被認為是好的實踐。現在我們卻努力對數據進行封裝。10年前,實現出不用考慮異常影響的函數被認為是好的實踐。現在我們努力寫出異常安全的代碼。
與時俱進。活到老,學到老。
6. 總結