C++箴言:爭取異常安全的代碼

來源:互聯網
上載者:User
C++箴言:爭取異常安全的代碼 異常安全(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 也已經被增加。(在另一方面,舊的映像被明確地刪除,所以我料想你會爭辯說映像已經被“改變”了。)

  規避資源流失問題比較容易,我們以前解釋了如何使用對象管理資源,也討論了引進 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 函數。)

   異常安全函數必須提供上述三種保證中的一種。如果它沒有提供,它就不是異常安全的。於是,選擇就在於決定你寫的每一個函數究竟要提供哪種保證。除非要處 理遺留下來的非異常安全的代碼(稍後我們要討論這個問題),只有當你的最高明的需求分析團隊為你的應用程式識別出的一項需求就是泄漏資源以及運行於被破壞 的資料結構之上時,不提供異常安全保證才能成為一個選項。

  作為一個一般性的規則,你應該提供實際可達到的最強力的保證。從異常安全的 觀點看,不拋出的函數(nothrow functions)是極好的,但是在 C++ 的 C 部分之外部不調用可能拋出異常的函數簡直就是寸步難行。使用動態分配記憶體的任何東西(例如,所有的 STL 容器)如果不能找到足夠的記憶體來滿足一個請求,在典型情況下,它就會拋出一個 bad_alloc 異常。只要你能做到就提供不拋出保證,但是對於大多數函數,選擇是在基本的保證和強力的保證之間的。

  在 changeBackground 的情況下,提供差不多的強力保證並不困難。首先,我們將 PrettyMenu 的 bgImage 資料成員的類型從一個內建的 Image* 指標改變為 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)的方式書寫的 代碼。

  沒有理由讓事情的這種狀態永遠持續下去。當書寫新的代碼或改變現存代碼時,要仔細考慮如何使它異常安全。以使用對象管理資源開 始。這樣可以防止資源泄漏。接下來,決定三種異常安全保證中的哪一種是你實際上能夠為你寫的每一個函數提供的最強的保證,只有當你不調用遺留代碼就別無選 擇的時候,才能滿足於沒有保證。既是為你的函數的客戶也是為了將來的維護人員,文檔化你的決定。一個函數的異常安全保證是它的介面的可見部分,所以你應該 特意選擇它,就像你特意選擇一個函數介面的其它方面。

  四十年前,到處都是 goto 的代碼被尊為最佳實務。現在我們為書寫結構化控制流程程而奮鬥。二十年前,全域可訪問資料被尊為最佳實務。現在我們為封裝資料而奮鬥,十年以前,寫函數時不必考慮異常的影響被尊為最佳實務。現在我們為寫異常安全的代碼而奮鬥。

  時光在流逝。我們生活著。我們學習著。

  Things to Remember

  ·即使當異常被拋出時,異常安全的函數不會泄露資源,也不允許資料結構被惡化。這樣的函數提供基本的,強力的,或者不拋出保證。

  ·強力保證經常可以通過 copy-and-swap 被實現,但是強力保證並非對所有函數都可用。

  ·一個函數通常能提供的保證不會強於他所調用的函數中最弱的保證。


天極流量聯盟免費換  我頂一下 我要挑錯 收藏到天極收藏夾 複製連結發給好友 加入收藏 列印 關閉 

網友評論 共有 0 條評論

歡迎評論!

發表評論您還能輸入300字 恭喜,資訊提交成功!


相關搜尋:

 

如何編寫異常安全的C++代碼

   收藏到:

發布時間:2007-12-9 15:25:28特性,幸運的是,隨著C++社區經驗的積累,今天我們已經有足夠的知識輕鬆編寫異常安全的代碼了,而且編寫異常安全的代碼一般也不會對效能造成影響。

  使用異常還是返回錯誤碼?這是個爭論不休的話題。大家一定聽說過這樣的說法:只有在真正異常的時候,才使用異常。那什麼是“真正異常的時候”?在回答這個問題以前,讓我們先看一看程式設計中的不變式原理。

  對象就是屬性彙總加方法,如何判定一個對象的屬性彙總是不是處於邏輯上正確的狀態呢?這可以通過一系列的斷言,最後下一個結論說:這個對象的屬性彙總邏輯上是正確的或者是有問題的。這些斷言就是衡量對象屬性彙總對錯的不變式。

  我們通常在函數調用中,實施不變式的檢查。不變式分為三類:前條件,後條件和不變式。前條件是指在函數調用之前,必須滿足的邏輯條件,後條件 是函數調用後必須滿足的邏輯條件,不變式則是整個函數執行中都必須滿足的條件。在我們的討論中,不變式既是前條件又是後條件。前條件是必須滿足的,如果不 滿足,那就是程式邏輯錯誤,後條件則不一定。現在,我們可以用不變式來嚴格定義異常狀況了:滿足前條件,但是無法滿足後條件,即為異常狀況。若且唯若發生 異常狀況時,才拋出異常。

  關於何時拋出異常的回答中,並不排斥傳回值報告錯誤,而且這兩者是正交的。然而,從我們經驗上來說,完全可以在這兩者中加以選擇,這又是為什 麼呢?事實上,當我們做出這種選擇時,必然意味著介面語意的改變,在不改變介面的情況下,其實是無法選擇的(試試看,用傳回值處理建構函式中的錯誤)。通 過不變式區別出正常和異常狀況,還可以更好地提煉介面。

  對於異常安全的評定,可分為三個層級:基本保證、強保證和不會失敗。

  基本保證:確保出現異常時程式(對象)處於未知但有效狀態。所謂有效,即對象的不變式檢查全部通過。

  強保證:確保操作的事務性,要麼成功,程式處於目標狀態,要麼不發生改變。

  不會失敗:對於大多數函數來說,這是很難保證的。對於C++程式,至少解構函式、釋放函數和swap函數要確保不會失敗,這是編寫異常安全的程式碼的基礎。

  首先從異常情況下資源管理的問題開始.很多人可能都這麼幹過:

  Type* obj = new Type;

  try{ do_something...}

  catch(...){ delete obj; throw;}

  不要這麼做!這麼做只會使你的代碼看上去混亂,而且會降低效率,這也是一直以來異常名聲不大好的原因之一. 請藉助於RAII技術來完成這樣的工作:

  auto_ptr obj_ptr(new Type);

  do_something...

  這樣的代碼簡潔、安全而且無損於效率。當你不關心或是無法處理異常時,請不要試圖捕獲它。並非使用try...catch才能編寫異常安全的 代碼,大部分異常安全的代碼都不需要try...catch。我承認,現實世界並非總是如上述的例子那樣簡單,但是這個例子確實可以代表很多異常安全的程式碼 的做法。在這個例子中,boost::scoped_ptr是auto_ptr一個更適合的替代品。

  現在來考慮這樣一個建構函式:

  Type() : m_a(new TypeA), m_b(new TypeB){}

  假設成員變數m_a和m_b是原始的指標類型,並且和Type內的申明順序一致。這樣的代碼是不安全的,它存在資源泄漏問題,建構函式的失敗 復原機制無法應對這樣的問題。如果new TypeB拋出異常,new TypeA返回的資源是得不到釋放機會的.曾經,很多人用這樣的方法避免異常:

   Type() : m_a(NULL), m_b(NULL){

   auto_ptr tmp_a(new TypeA);

   auto_ptr tmp_b(new TypeB);

   m_a = tmp_a.release();

   m_b = tmp_b.release();

   }

  當然,這樣的方法確實是能夠實現異常安全的代碼的,而且其中實現思想將是非常重要的,在如何?強保證的異常安全的程式碼中會採用這種思想.然而 這種做法不夠徹底,至少解構函式還是要手動完成的。我們仍然可以藉助RAII技術,把這件事做得更為徹底:shared_ptr m_a; shared_ptr m_b;這樣,我們就可以輕而易舉地寫出異常安全的代碼:

  Type() : m_a(new TypeA), m_b(new TypeB){}

  如果你覺得shared_ptr的效能不能滿足要求,可以編寫一個介面類似scoped_ptr的智能指標類,在解構函式中釋放資源即可。如 果類設計成不可複製的,也可以直接用scoped_ptr。強烈建議不要把auto_ptr作為資料成員使用,scoped_ptr雖然名字不大好,但是 至少很安全而且不會導致混亂。

   RAII技術並不僅僅用於上述例子中,所有必須成對出現的操作都可以通過這一技術完成而不必try...catch.下面的代碼也是常見的:

  a_lock.lock();

  try{ ...} catch(...) {a_lock.unlock();throw;}

  a_lock.unlock();

  可以這樣解決,先提供一個成對操作的輔助類:

  struct scoped_lock{

   explicit scoped_lock(Lock lock) : m_l(lock){m_l.lock();}

   ~scoped_lock(){m_l.unlock();}

   private:

   Lock m_l;

   };

  然後,代碼只需這樣寫:

  scoped_lock guard(a_lock);

  do_something...

  清晰而優雅!繼續考察這個例子,假設我們並不需要成對操作, 顯然,修改scoped_lock建構函式即可解決問題。然而,往往方法名稱和參數也不是那麼固定的,怎麼辦?可以藉助這樣一個輔助類:

   template

   struct pair_guard{

   pair_guard(FEnd fe, FBegin fb) : m_fe(fe) {if (fb) fb();}

   ~pair_guard(){m_fe();}

   private:

   FEnd m_fe;

   ...//禁止複製

   };

  typedef pair_guard , function > simple_pair_guard;

  好了,藉助boost庫,我們可以這樣來編寫代碼了:

  simple_pair_guard guard(bind(Lock::unlock, a_lock), bind(Lock::lock, a_lock) );

  do_something...

  我承認,這樣的代碼不如前面的簡潔和容易理解,但是它更靈活,無論函數名稱是什麼,都可以拿來結對。我們可以加強對bind的運用,結合佔位 符和reference_wrapper,就可以處理函數參數、動態綁定變數。所有我們在catch內外的相同工作,交給pair_guard去完成即 可。

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.