Item 29: 爭取 exception-safe code(異常安全的程式碼)
作者:Scott Meyers
譯者:fatalerror99 (iTePub's Nirvana)
發布:http://blog.csdn.net/fatalerror99/
exception safety(異常安全)有點像 pregnancy(懷孕)……但是,請把這個想法先保留一會兒。我們還不能真正地議論 reproduction(生育),直到我們排除萬難渡過 courtship(求愛時期)。(此段作者使用的 3 個詞均有雙關含義,pregnancy 也可理解為富有意義,reproduction 也可理解為再現,再生,courtship 也可理解為爭取,謀求。為了與後面的譯文對應,故按照現在的譯法。——譯者注)
假設我們有一個 class,代錶帶有背景映像的 GUI 菜單。這個 class 被設計用於一個 threaded environment(多線程環境),所以它有一個用於 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
}
從 exception safety(異常安全)的觀點看,這個函數爛到了極點。exception safety(異常安全)有兩條要求,而這裡全都沒有滿足。
當一個 exception(異常)被拋出,exception-safe functions(異常安全函數)應該:
- Leak no resources(沒有資源流失)。上面的代碼沒有通過這個測試,因為如果 "new Image(imgSrc)" 運算式引發一個 exception(異常),對 unlock 的調用就永遠不會執行,而那個 mutex(互斥體)也將被永遠掛起。
- Don't allow data structures to become corrupted(不允許資料結構被破壞)。如果 "new Image(imgSrc)" throws(拋出異常),bgImage 被留下來指向一個已刪除 object。另外,儘管並沒有將一張新的映像設定到位,imageChanges 也已經被增加。(在另一方面,舊的映像被明確地刪除,所以我料想你會爭辯說映像已經被“改變”了。)
規避 resource leak(資源流失)問題比較容易,因為 Item 13 解釋了如何使用 objects 管理資源,而 Item 14 又引進了 Lock class 作為一個確保互斥體被及時恰當地釋放的方法:
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 這樣的 resource management classes(資源管理類)的最好的事情之一是它們通常會使函數變短。看到如何使對 unlock 的調用不再需要了嗎?作為一個一般的規則,更少的代碼就是更好的代碼,因為在改變的時候這樣可以較少誤入歧途並較少產生誤解。
隨著 resource leak(資源流失)被我們甩在身後,我們可以把我們的注意力集中到 data structure corruption(資料結構被破壞)的問題。在這裡我們有一個選擇,但是在我們能選擇之前,我們必須先面對定義我們的選擇的術語。
exception-safe functions(異常安全函數)提供下述三種保證之一:
- 函數提供 the basic guarantee(基本保證),允諾如果一個異常被拋出,程式中剩下的每一件東西都處於合法狀態。沒有 objects 或資料結構被破壞,而且所有的 objects 都處於內部調和狀態(所有的 class invariants(類不變數)都被滿足)。然而,程式的精確狀態可能是不可預言的。例如,我們可以重寫 changeBackground,以便於在一個異常被拋出時,PrettyMenu object 可以繼續保留原來的背景映像,或者它可以持有某些預設的背景映像,但是客戶無法預知到底是哪一個。(為了查明這一點,他們大概必須調用某個可以告訴他們當前背景映像是什麼的 member function。)
- 函數提供 the strong guarantee(強力保證),允諾如果一個異常被拋出,程式的狀態不會發生變化。調用這樣的函數在感覺上是 atomic(原子)的,如果它們成功了,它們就完全成功,如果它們失敗了,程式的狀態就像它們從沒有被調用過一樣。
與提供 strong guarantee(強力保證)的函數一起工作比與只提供 basic guarantee(基本保證)的函數一起工作更加容易,因為調用提供 strong guarantee(強力保證)的函數之後,僅有兩種可能的程式狀態:像預期一樣成功執行了函數,或者繼續保持函數被調用時當時的狀態。與之相比,如果調用一個只提供 basic guarantee(基本保證)的函數引發了異常,程式可能存在於任何合法的狀態。
- 函數提供 the nothrow guarantee(不拋出保證),允諾決不拋出異常,因為它們只做它們保證能做到的。所有對 built-in types(內建類型)(例如,ints,指標,等等)的操作都是 nothrow(不拋出)的(也就是說,提供 nothrow guarantee(不拋出保證))。這是 exception-safe code(異常安全的程式碼)中必不可少的基礎構件。
假設一個帶有 empty exception specification(空異常規格)的函數是不拋出的似乎是合理的,但這不是一定成立的。例如,考慮這個函數:
int doSomething() throw(); // note empty exception spec.
這並不是說 doSomething 永遠都不會拋出異常;而是說如果 doSomething 拋出一個異常,它就是一個嚴重的錯誤,應該調用 unexpected function [1]。實際上,doSomething 可能根本不提供任何異常保證。一個函數的聲明(如果有的話,也包括它的 exception specification(異常規格))不能告訴你一個函數是否正確,是否可移植,或是否高效,而且,即便有,它也不能告訴你它會提供哪一種 exception safety guarantee(異常安全保證)。所有這些特性都由函數的實現決定,而不是它的聲明。
[1] 關於 unexpected function 的資料,可以求助於你中意的搜尋引擎或包羅永珍的 C++ 課本。(你或許有幸搜到 set_unexpected,這個函數用於指定 unexpected function。)
exception-safe code(異常安全的程式碼)必須提供上述三種保證中的一種。如果它沒有提供,它就不是 exception-safe(異常安全)的。於是,選擇就在於決定你寫的每一個函數究竟要提供哪種保證。除非要處理 exception-unsafe(異常不安全)的遺留代碼(本 Item 稍後我們要討論這個問題),只有當你的最高明的需求分析團隊為你的應用程式識別出的一項需求就是泄漏資源以及運行於被破壞的資料結構之上時,不提供 exception safety guarantee(異常安全保證)才能成為一個選項。
作為一個一般性的規則,你應該提供實際可達到的最強力的保證。從 exception safety(異常安全)的觀點看,nothrow functions(不拋出的函數)是極棒的,但是在 C++ 的 C 部分之外不調用可能拋出異常的函數簡直就是寸步難行。使用動態分配記憶體的任何東西(例如,所有的 STL containers)如果不能找到足夠的記憶體來滿足一個請求(參見 Item 49),在典型情況下,它就會拋出一個 bad_alloc 異常。只要你能做到就提供 nothrow guarantee(不拋出保證),但是對於大多數函數,選擇是在基本保證和強力保證之間的。
在 changeBackground 的情況下,提供 almost(差不多)的 strong guarantee(強力保證)並不困難。首先,我們將 PrettyMenu 的 bgImage data member 的類型從一個 built-in Image* pointer(指標)改變為 Item 13 中描述的 smart resource-managing pointers(智能資源管理指標)中的一種。坦白地講,在預防資源泄漏的基本原則上,這完全是一個好主意。它協助我們提供 strong exception safety guarantee(強力異常安全保證)的事實進一步加強了 Item 13 的論點——使用 objects(諸如 smart pointers(智能指標))管理資源是良好設計的基礎。在下面的代碼中,我展示了 tr1::shared_ptr 的使用,因為當進行通常的拷貝時它的行為更符合直覺,這使得它比 auto_ptr 更可取。
第二,我們重新排列 changeBackground 中的語句,以便於直到映像發生變化,才增加 imageChanges。作為一個一般規則,這是一個很好的策略——直到某件事情真正發生了,再改變一個 object 的狀態來表示某事已經發生。
這就是修改之後的代碼:
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;
}
注意這裡不再需要手動刪除舊的映像,因為這些已經由 smart pointer(智能指標)在內部處理了。此外,只有當新的映像被成功建立了刪除行為才會發生。更準確地說,只有當 tr1::shared_ptr::reset 函數的參數("new Image(imgSrc)" 的結果)被成功建立了,這個函數才會被調用。只有在 reset 的調用中才會使用 delete,所以如果這個函數從來不曾進入,delete 就從來不曾使用。同時請注意一個管理資源(動態分配的 Image)的 object (tr1::shared_ptr) 的使用又一次縮短了 changeBackground 的長度。
正如我所說的,這兩處改動 almost(差不多)有能力使 changeBackground 提供 strong exception safety guarantee(強力異常安全保證)。美中不足的是什麼呢?參數 imgSrc。如果 Image constructor(建構函式)拋出一個異常,input stream(輸入資料流)的讀標記就有可能已經被移動,而這樣的移動就成為一個對程式的其它部分來說可見的狀態變化。直到 changeBackground 著手解決這個問題之前,它只能提供 basic exception safety guarantee(基本異常安全保證)。
(本篇未完,點擊此處,接下篇)