# Item 29: 爭取異常安全(exception-safe)的代碼
作者:Scott Meyers
譯者:fatalerror99 (iTePub's Nirvana)
發布:http://blog.csdn.net/fatalerror99/
異常安全(Exception safety)有點像懷孕(pregnancy)……但是,請把這個想法先控制一會兒。我們還不能真正地議論生育(reproduction),直到我們排除萬難渡過求愛時期(courtship)。(此段作者使用的 3 個詞均有雙關含義,pregnancy 也可理解為富有意義,reproduction 也可理解為再現,再生,courtship 也可理解為爭取,謀求。為了與后面的譯文對應,故按照現在的譯法。——譯者注)
假設我們有一個類,代表帶有背景圖像的 GUI 菜單。這個類被設計成在多線程環境中使用,所以它有一個用于并行控制(concurrency control)的互斥體(mutex):
```
class PrettyMenu {
public:
...
void changeBackground(std::istream& imgSrc); // change background
... // image
private:
Mutex mutex; // mutex for this object
Image *bgImage; // current background image
int imageChanges; // # of times image has been changed
};
```
考慮這個 PrettyMenu 的 changeBackground 函數的可能的實現:
```
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
lock(&mutex); // acquire mutex (as in Item 14)
delete bgImage; // get rid of old background
++imageChanges; // update image change count
bgImage = new Image(imgSrc); // install new background
unlock(&mutex); // release mutex
}
```
從異常安全的觀點看,這個函數爛到了極點。異常安全有兩條要求,而這里全都沒有滿足。
當一個異常被拋出,異常安全的函數應該:
* 沒有資源泄露。上面的代碼沒有通過這個測試,因為如果 "new Image(imgSrc)" 表達式產生一個異常,對 unlock 的調用就永遠不會執行,而那個互斥體也將被永遠掛起。
* 不允許數據結構惡化。如果 "new Image(imgSrc)" 拋出異常,bgImage 被遺留下來指向一個被刪除對象。另外,盡管并沒有將一張新的圖像設置到位,imageChanges 也已經被增加。(在另一方面,舊的圖像被明確地刪除,所以我料想你會爭辯說圖像已經被“改變”了。)
規避資源泄露問題比較容易,因為 Item 13 解釋了如何使用對象管理資源,而 Item 14 又引進了 Lock 類作為一種時尚的確保互斥體被釋放的方法:
```
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock ml(&mutex); // from Item 14: acquire mutex and
// ensure its later release
delete bgImage;
++imageChanges;
bgImage = new Image(imgSrc);
}
```
關于像 Lock 這樣的資源管理類的最好的事情之一是它們通常會使函數變短。看到對 unlock 的調用不再需要了嗎?作為一個一般的規則,更少的代碼就是更好的代碼。因為在改變的時候這樣可以較少誤入歧途并較少產生誤解。
隨著資源泄露被我們甩在身后,我們可以把我們的注意力集中到數據結構惡化。在這里我們有一個選擇,但是在我們能選擇之前,我們必須先面對定義我們的選擇的術語。
異常安全函數提供下述三種保證之一:
* 函數提供基本保證(the basic guarantee),允諾如果一個異常被拋出,程序中剩下的每一件東西都處于合法狀態。沒有對象或數據結構被破壞,而且所有的對象都處于內部調和狀態(所有的類不變量都被滿足)。然而,程序的精確狀態可能是不可預期的。例如,我們可以重寫 changeBackground,以致于如果一個異常被拋出,PrettyMenu 對象可以繼續保留原來的背景圖像,或者它可以持有某些缺省的背景圖像,但是客戶無法預知到底是哪一個。(為了查明這一點,他們大概必須調用某個可以告訴他們當前背景圖像是什么的成員函數。)
* 函數提供強力保證(the strong guarantee),允諾如果一個異常被拋出,程序的狀態不會發生變化。調用這樣的函數在感覺上是極其微弱的,如果它們成功了,它們就完全成功,如果它們失敗了,程序的狀態就像它們從沒有被調用過一樣。
與提供強力保證的函數一起工作比與只提供基本保證的函數一起工作更加容易,因為調用提供強力保證的函數之后,僅有兩種可能的程序狀態:像預期一樣成功執行了函數,或者繼續保持函數被調用時當時的狀態。與之相比,如果調用只提供基本保證的函數引發了異常,程序可能存在于任何合法的狀態。
* 函數提供不拋出保證(the nothrow guarantee),允諾決不拋出異常,因為它們只做它們答應要做的。所有對內建類型(例如,ints,指針,等等)的操作都是不拋出(nothrow)的(也就是說,提供不拋出保證)。這是異常安全代碼中必不可少的基礎構件。
假定一個帶有空的異常規格(exception specification)的函數是不拋出的似乎是合理的,但這不一定正確的。例如,考慮這個函數:
```
int doSomething() throw(); // note empty exception spec.
```
這并不是說 doSomething 永遠不會拋出異常;而是說如果 doSomething 拋出一個異常,它就是一個嚴重的錯誤,應該調用 unexpected 函數 [1]。實際上,doSomething 可能根本不提供任何異常保證。一個函數的聲明(如果有的話,也包括它的異常規格(exception specification))不能告訴你一個函數是否正確,是否可移植,或是否高效,而且,即便有,它也不能告訴你它會提供哪一種異常安全保證。所有這些特性都由函數的實現決定,而不是它的聲明能決定的。
[1] 關于 unexpected 函數的資料,可以求助于你中意的搜索引擎或包羅萬象的 C++ 課本。(你或許有幸搜到 set_unexpected,這個函數用于指定 unexpected 函數。)
異常安全函數必須提供上述三種保證中的一種。如果它沒有提供,它就不是異常安全的。于是,選擇就在于決定你寫的每一個函數究竟要提供哪種保證。除非要處理遺留下來的非異常安全的代碼(本 Item 稍后我們要討論這個問題),只有當你的最高明的需求分析團隊為你的應用程序識別出的一項需求就是泄漏資源以及運行于被破壞的數據結構之上時,不提供異常安全保證才能成為一個選項。
作為一個一般性的規則,你應該提供實際可達到的最強力的保證。從異常安全的觀點看,不拋出的函數(nothrow functions)是極好的,但是在 C++ 的 C 部分之外部不調用可能拋出異常的函數簡直就是寸步難行。使用動態分配內存的任何東西(例如,所有的 STL 容器)如果不能找到足夠的內存來滿足一個請求(參見 Item 49),在典型情況下,它就會拋出一個 bad_alloc 異常。只要你能做到就提供不拋出保證,但是對于大多數函數,選擇是在基本的保證和強力的保證之間的。
在 changeBackground 的情況下,提供差不多的強力保證并不困難。首先,我們將 PrettyMenu 的 bgImage 數據成員的類型從一個內建的 Image\* 指針改變為 Item 13 中描述的智能資源管理指針中的一種。坦白地講,在預防資源泄漏的基本原則上,這完全是一個好主意。它幫助我們提供強大的異常安全保證的事實進一步加強了 Item 13 的論點——使用對象(諸如智能指針)管理資源是良好設計的基礎。在下面的代碼中,我展示了 tr1::shared_ptr 的使用,因為當進行通常的拷貝時它的更符合直覺的行為使得它比 auto_ptr 更可取。
第二,我們重新排列 changeBackground 中的語句,以致于直到圖像發生變化,才增加 imageChanges。這是一個很好的策略——直到某件事情真正發生了,再改變一個對象的狀態來表示某事已經發生。
這就是修改之后的代碼:
```
class PrettyMenu {
...
std::tr1::shared_ptr<Image> bgImage;
...
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
Lock ml(&mutex);
bgImage.reset(new Image(imgSrc)); // replace bgImage's internal
// pointer with the result of the
// "new Image" expression
++imageChanges;
}
```
注意這里不再需要手動刪除舊的圖像,因為在智能指針內部已經被處理了。此外,只有當新的圖像被成功創建了刪除行為才會發生。更準確地說,只有當 tr1::shared_ptr::reset 函數的參數("new Image(imgSrc)" 的結果)被成功創建了,這個函數才會被調用。只有在對 reset 的調用的內部才會使用 delete,所以如果這個函數從來不曾進入,delete 就從來不曾使用。同樣請注意一個管理資源(動態分配的 Image)的對象(tr1::shared_ptr)的使用再次縮短了 changeBackground 的長度。
正如我所說的,這兩處改動差不多有能力使 changeBackground 提供強力異常安全保證。美中不足的是什么呢?參數 imgSrc。如果 Image 的構造函數拋出一個異常,輸入流(input stream)的讀標記(read marker)可能已經被移動,而這樣的移動就成為對程序的其它部分來說可見的一個狀態的變化。直到 changeBackground 著手解決這個問題之前,它只能提供基本異常安全保證。
無論如何,讓我們把它放在一邊,并且依然假裝 changeBackground 可以提供強力保證。(我相信你至少能用一種方法做到這一點,或許可以通過將它的參數從一個 istream 改變到包含圖像數據的文件的文件名。)有一種通常的設計策略可以有代表性地產生強力保證,而且熟悉它是非常必要的。這個策略被稱為 "copy and swap"。它的原理很簡單。先做出一個你要改變的對象的拷貝,然后在這個拷貝上做出全部所需的改變。如果改變過程中的某些操作拋出了異常,最初的對象保持不變。在所有的改變完全成功之后,將被改變的對象和最初的對象在一個不會拋出異常的操作中進行交換。
這通常通過下面的方法實現:將每一個對象中的全部數據從“真正的”對象中放入到一個單獨的實現對象中,然后將一個指向實現對象的指針交給真正對象。這通常被稱為 "pimpl idiom",Item 31 描述了它的一些細節。對于 PrettyMenu 來說,它一般就像這樣:
```
struct PMImpl { // PMImpl = "PrettyMenu
std::tr1::shared_ptr<Image> bgImage; // Impl."; see below for
int imageChanges; // why it's a struct
};
class PrettyMenu {
...
private:
Mutex mutex;
std::tr1::shared_ptr<PMImpl> pImpl;
};
void PrettyMenu::changeBackground(std::istream& imgSrc)
{
using std::swap; // see Item 25
Lock ml(&mutex); // acquire the mutex
std::tr1::shared_ptr<PMImpl> // copy obj. data
pNew(new PMImpl(*pImpl));
pNew->bgImage.reset(new Image(imgSrc)); // modify the copy
++pNew->imageChanges;
swap(pImpl, pNew); // swap the new
// data into place
} // release the mutex
```
在這個例子中,我選擇將 PMImpl 做成一個結構體,而不是類,因為通過讓 pImpl 是 private 就可以確保 PrettyMenu 數據的封裝。將 PMImpl 做成一個類雖然有些不那么方便,卻沒有增加什么好處。(這也會使有面向對象潔癖者走投無路。)如果你愿意,PMImpl 可以嵌套在 PrettyMenu 內部,像這樣的打包問題與我們這里所關心的寫異常安全的代碼的問題沒有什么關系。
copy-and-swap 策略是一種全面改變或絲毫不變一個對象的狀態的極好的方法,但是,在通常情況下,它不能保證全部函數都是強力異常安全的。為了弄清原因,考慮一個 changeBackground 的抽象化身—— someFunc,它使用了 copy-and-swap,但是它包含了對另外兩個函數(f1 和 f2)的調用:
```
void someFunc()
{
... // make copy of local state
f1();
f2();
... // swap modified state into place
}
```
很明顯,如果 f1 或 f2 低于強力異常安全,someFunc 就很難成為強力異常安全的。例如,假設 f1 僅提供基本保證。為了讓 someFunc 提供強力保證,它必須寫代碼在調用 f1 之前測定整個程序的狀態,并捕捉來自 f1 的所有異常,然后恢復到最初的狀態。
即使 f1 和 f2 都是強力異常安全的,事情也好不到哪去。如果 f1 運行完成,程序的狀態已經發生了毫無疑問的變化,所以如果隨后 f2 拋出一個異常,即使 f2 沒有改變任何東西,程序的狀態也已經和調用 someFunc 時不同。
問題在于副作用。只要函數僅對局部狀態起作用(例如,someFunc 僅僅影響調用它的那個對象的狀態),它提供強力保證就相對容易。當函數的副作用影響了非局部數據,它就會困難得多。例如,如果調用 f1 的副作用是改變數據庫,讓 someFunc 成為強力異常安全就非常困難。一般情況下,沒有辦法撤銷已經提交的數據庫變化,其他數據庫客戶可能已經看見了數據庫的新狀態。
類似這樣的問題會阻止你為函數提供強力保證,即使你希望去做。另一個問題是效率。copy-and-swap 的要點是這樣一個想法:改變一個對象的數據的拷貝,然后在一個不會拋出異常的操作中將被改變的數據和原始數據進行交換。這就需要做出每一個要改變的對象的拷貝,這可能會用到你不能或不情愿動用的時間和空間。強力保證是非常值得的,當它可用時你應該提供它,除非在它不能 100% 可用的時候。
當它不可用時,你就必須提供基本保證。在實踐中,你可能會發現你能為某些函數提供強力保證,但是效率和復雜度的成本使得它難以支持大量的其它函數。無論何時,只要你作出過一個提供強力保證的合理的成果,就沒有人會因為你僅僅提供了基本保證而站在批評你的立場上。對于很多函數來說,基本保證是一個完全合理的選擇。
如果你寫了一個根本沒有提供異常安全保證的函數,事情就不同了,因為在這一點上有罪推定是合情合理的,直到你證明自己是清白的。你應該寫出異常安全的代碼。除非你能做出有說服力的答辯。請再次考慮 someFunc 的實現,它調用了函數 f1 和 f2。假設 f2 根本沒有提供異常安全保證,甚至沒有基本保證。這就意味著如果 f2 發生一個異常,程序可能會在 f2 內部泄漏資源。這也意味著 f2 可能會惡化數據結構,例如,已排序數組可能不再排序,一個正在從一個數據結構傳送到另一個數據結構去的對象可能丟失,等等。沒有任何辦法可以讓 someFunc 能彌補這些問題。如果 someFunc 調用的函數不提供異常安全保證,someFunc 本身就不能提供任何保證。
請允許我回到懷孕。一個女性或者懷孕或者沒有。局部懷孕是絕不可能的。與此相似,一個軟件或者是異常安全的或者不是。沒有像一個局部異常安全的系統這樣的東西。一個系統即使只有一個函數不是異常安全的,那么系統作為一個整體就不是異常安全的,因為調用那個函數可能發生泄漏資源和惡化數據結構。不幸的是,很多 C++ 的遺留代碼在寫的時候沒有留意異常安全,所以現在的很多系統都不是異常安全的。它們混合了用非異常安全(exception-unsafe)的方式書寫的代碼。
沒有理由讓事情的這種狀態永遠持續下去。當書寫新的代碼或改變現存代碼時,要仔細考慮如何使它異常安全。以使用對象管理資源開始。(還是參見 Item 13。)這樣可以防止資源泄漏。接下來,決定三種異常安全保證中的哪一種是你實際上能夠為你寫的每一個函數提供的最強的保證,只有當你不調用遺留代碼就別無選擇的時候,才能滿足于沒有保證。既是為你的函數的客戶也是為了將來的維護人員,文檔化你的決定。一個函數的異常安全保證是它的接口的可見部分,所以你應該特意選擇它,就像你特意選擇一個函數接口的其它方面。
四十年前,到處都是 goto 的代碼被尊為最佳實踐。現在我們為書寫結構化控制流程而奮斗。二十年前,全局可訪問數據被尊為最佳實踐。現在我們為封裝數據而奮斗,十年以前,寫函數時不必考慮異常的影響被尊為最佳實踐。現在我們為寫異常安全的代碼而奮斗。
時光在流逝。我們生活著。我們學習著。
Things to Remember
* 即使當異常被拋出時,異常安全的函數不會泄露資源,也不允許數據結構被惡化。這樣的函數提供基本的,強力的,或者不拋出保證。
* 強力保證經常可以通過 copy-and-swap 被實現,但是強力保證并非對所有函數都可用。
* 一個函數通常能提供的保證不會強于他所調用的函數中最弱的保證。
- Preface(前言)
- Introduction(導言)
- Terminology(術語)
- Item 1: 將 C++ 視為 federation of languages(語言聯合體)
- Item 2: 用 consts, enums 和 inlines 取代 #defines
- Item 3: 只要可能就用 const
- Item 4: 確保 objects(對象)在使用前被初始化
- Item 5: 了解 C++ 為你偷偷地加上和調用了什么函數
- Item 6: 如果你不想使用 compiler-generated functions(編譯器生成函數),就明確拒絕
- Item 7: 在 polymorphic base classes(多態基類)中將 destructors(析構函數)聲明為 virtual(虛擬)
- Item 8: 防止因為 exceptions(異常)而離開 destructors(析構函數)
- Item 9: 絕不要在 construction(構造)或 destruction(析構)期間調用 virtual functions(虛擬函數)
- Item 10: 讓 assignment operators(賦值運算符)返回一個 reference to *this(引向 *this 的引用)
- Item 11: 在 operator= 中處理 assignment to self(自賦值)
- Item 12: 拷貝一個對象的所有組成部分
- Item 13: 使用對象管理資源
- Item 14: 謹慎考慮資源管理類的拷貝行為
- Item 15: 在資源管理類中準備訪問裸資源(raw resources)
- Item 16: 使用相同形式的 new 和 delete
- Item 17: 在一個獨立的語句中將 new 出來的對象存入智能指針
- Item 18: 使接口易于正確使用,而難以錯誤使用
- Item 19: 視類設計為類型設計
- Item 20: 用 pass-by-reference-to-const(傳引用給 const)取代 pass-by-value(傳值)
- Item 21: 當你必須返回一個對象時不要試圖返回一個引用
- Item 22: 將數據成員聲明為 private
- Item 23: 用非成員非友元函數取代成員函數
- Item 24: 當類型轉換應該用于所有參數時,聲明為非成員函數
- Item 25: 考慮支持不拋異常的 swap
- Item 26: 只要有可能就推遲變量定義
- Item 27: 將強制轉型減到最少
- Item 28: 避免返回對象內部構件的“句柄”
- Item 29: 爭取異常安全(exception-safe)的代碼
- Item 30: 理解 inline 化的介入和排除
- Item 31: 最小化文件之間的編譯依賴
- Item 32: 確保 public inheritance 模擬 "is-a"
- Item 33: 避免覆蓋(hiding)“通過繼承得到的名字”
- Item 34: 區分 inheritance of interface(接口繼承)和 inheritance of implementation(實現繼承)
- Item 35: 考慮可選的 virtual functions(虛擬函數)的替代方法
- Item 36: 絕不要重定義一個 inherited non-virtual function(通過繼承得到的非虛擬函數)
- Item 37: 絕不要重定義一個函數的 inherited default parameter value(通過繼承得到的缺省參數值)
- Item 38: 通過 composition(復合)模擬 "has-a"(有一個)或 "is-implemented-in-terms-of"(是根據……實現的)
- Item 39: 謹慎使用 private inheritance(私有繼承)
- Item 40: 謹慎使用 multiple inheritance(多繼承)
- Item 41: 理解 implicit interfaces(隱式接口)和 compile-time polymorphism(編譯期多態)
- Item 42: 理解 typename 的兩個含義
- Item 43: 了解如何訪問 templatized base classes(模板化基類)中的名字
- Item 44: 從 templates(模板)中分離出 parameter-independent(參數無關)的代碼
- Item 45: 用 member function templates(成員函數模板) 接受 "all compatible types"(“所有兼容類型”)
- Item 46: 需要 type conversions(類型轉換)時在 templates(模板)內定義 non-member functions(非成員函數)
- Item 47: 為類型信息使用 traits classes(特征類)
- Item 48: 感受 template metaprogramming(模板元編程)
- Item 49: 了解 new-handler 的行為
- Item 50: 領會何時替換 new 和 delete 才有意義
- Item 51: 編寫 new 和 delete 時要遵守慣例
- Item 52: 如果編寫了 placement new,就要編寫 placement delete
- 附錄 A. 超越 Effective C++
- 附錄 B. 第二和第三版之間的 Item 映射