C與C++中的異常處理13 (轉)

gugu99發表於2008-08-06
C與C++中的異常處理13 (轉)[@more@]

:namespace prefix = o ns = "urn:schemas--com::office" />

  接下來兩次,我將討論“異常安全”,C++標準中使用了(在auto_ptr中)卻沒有定義的術語。在C++範圍內,不同的作者使用這個術語卻表達不同的含義。在我的專題中,我從兩個方面來定義“異常安全”:

l  如果一個實體捕獲或丟擲一個異常,但仍然維持它公開保證的語義,它就是“介面安全”的。依賴於它保證的力度,實體可能不允許將任何異常漏給其。

l  如果異常沒有導致資源洩漏或產生未定義的行為,實體就是“行為安全”的。“行為安全”一般是強迫的。幸運的是,如果做到了“行為安全”,通常也間接提供了“介面安全”。

  異常安全有點象const:好的設計必須在一開始就考慮它,它不能夠事後補救。但是我們開始使用異常還沒有多少年,所以還沒有“異常安全問題集”這樣的東西來指導我們。實際上,我期望大家透過一條艱辛的道路來掌握異常安全:透過經歷異常故障在編碼時繞過它們;或關閉異常特性,認為它們“太難”被正確掌握。

  我不想撒謊:分析設計上的異常安全性太難了。但是,艱辛的工作也有豐厚的回報。不過,這個主題太難了,想面面俱到的話將花我幾個月的時間。我最小的目標是:透過缺乏異常安全的例子來展示怎麼使它們變得安全,並激勵你在此專題之外去看和學更多的東西。

 

1.1  構造

  如果一個普通的成員函式

x.f()

丟擲一個異常,你可以容忍此異常並試圖再次它:

X x;

bool done;

do

  {

  try

  {

  done = true;

  x.f();

  }

  catch (...)

  {

  // do something to recover, then retry

  done = false;

  }

  }

while (!done);

  但,如果你試圖再次呼叫一個建構函式,你實際上是呼叫了一個完全不同的:

bool done(false);

while (!done)

  {

  try

   {

  done = true;

  X x; // calls X::X()

  }

  // from this point forward, `x` does not exist

  catch (...)

  {

  // do something to recover, then retry

  done = false;

  }

  }

  你不能挽救一個建構函式拋異常的物件;異常的存在表明那個物件已經死了。

  當一個建構函式拋異常時,它殺死了其宿主物件而沒有呼叫解構函式。這樣的拋異常行為危害了“行為安全”:如果這個拋異常的建構函式分配了資源,你無法依賴解構函式釋放它們。一般構造和析構是成對的,並期待後者清理前者。如果解構函式沒有被呼叫,這個期望是不滿足的。

  最後,如果你從建構函式中拋了一個異常,並且你的類是使用者類的一個基類或子物件,那麼使用者類的建構函式必須處理你丟擲的異常。或者它將異常拋給另外一個使用者類的建構函式,如此遞推下去,直到呼叫tenate()。實際上使用者必須做你沒有做的工作(維持建構函式的安全性)。

 

1.2  關於取捨的問題

  建構函式拋異常同時降低了介面安全和行為安全。除非有迫不得以的理由,不要讓建構函式拋異常。

  也有不同的意見認為:異常應該被本來就做這事的專門程式碼捕獲的。那些只是靜靜地接收異常而沒有處理它們的異常處理函式違背了這些異常的初衷。如果一個函式沒有準備好正確地處理一個異常,它應該將這個異常傳遞下去。

  最低事實是:必須有人處理異常;如果所有人都放過它,程式將終止。還必須同時捕獲觸發異常的條件;如果沒人標記它,程式可能以任何方式終止,並且恐怕不怎麼文雅。

  一個異常物件警示我們存在一個不該忽略的錯誤狀況。不幸的是,這個物件的存在可能導致一個全新的不同的錯誤狀況。在設計異常安全的時候,你必須在兩個有時衝突的設計原則間進行取捨。

1.在錯誤發生時進行通報

2.防止這個通報行為導致其它錯誤。

  因為建構函式拋異常可能有有害的副作用,你必須小心權衡這兩個原則。我不允許我寫的建構函式中拋異常,這樣設計傾向於原則2;但我不想將它推薦為普遍原則,在其它情況下這兩個原則是等重的。自己好自判斷吧。

 

1.3  解構函式

  解構函式拋異常可能使程式有奇怪的反應。它可能徹底地殺死程式。根據C++標準(subclause 15.1.1,“the terminate() function” ),簡述如下:

  在某些情況下,異常處理必須被拋棄以減少一些微妙的錯誤。這些情況中包括:當因為異常而退棧過程中將要被析構的物件的解構函式。在這些情況下,函式void terminate()被呼叫。退棧不會完成。

  簡而言之,解構函式不該提示是否發生了異常。但,如我上次所說,新的C++標準執行庫程式uncaught_exception()可以讓解構函式確定其所處的異常環境。不幸的是,我上次也說了,Visual C++未能正確地支援這個函式。

  問題比我提示的還要糟。我上次寫到,Microsoft的uncaught_exception()函式版本一定返回false,所以Visaul C++總告訴你的解構函式當前沒有發生異常,在其中拋異常是可以的。如果你從一個支援uncaught_exception的環境轉到Visual C++,以前正常工作的程式碼可能開始呼叫terminate()了。

 

  要嘗試一下的話,試下面的例子:

#include

#include

#include

 

using namespace std;

 

static void my_terminate_handler(void)

  {

  printf("Library lied; I'm in the terminate handler.n");

  abort();

  }

 

class X

  {

public:

  ~X()

  {

  if (uncaught_exception())

  printf("Library says not to throw.n");

  else

  {

  printf("Library says I'm OK to throw.n");

  throw 0;

  }

  }

  };

 

int main()

  {

  set_terminate(my_terminate_handler);

  try

  {

  X x;

  throw 0;

  }

   catch (...)

  {

  }

  printf("Exiting normally.n");

  return 0;

  }

 

  在C++標準相容的環境下,你得到:

Library says not to throw.

Exiting normally.

 

  但Visual C++下,你得到:

Library says I'm OK to throw.

Library lied; I'm in the terminate handler.

並跟隨一個程式異常終止。

 

And with six you get egg roll.

  建議:除非你確切知道你現在及以後所用的平臺都正確支援uncaught_exception(),不要呼叫它。

 

1.4  部分刪除

  即使你知道當前不在處理異常,你仍然不應該在解構函式中拋異常。考慮如下的例子:

class X

  {

public:

  ~X()

  {

  throw 0;

  }

  };

 

int main()

  {

  X *x = new X;

  delete x;

  return 0;

  }

 

  當main()到delete x,如下兩步將依次發生:

x的解構函式被呼叫。

operator delete被呼叫了來釋放x的空間。

  但因為x的解構函式拋了異常,operator delete沒有被呼叫。這危及了行為安全。如果還不信,試一下這個更完整的例子:

 

#include

#include

 

class X

  {

public:

  ~X()

  {

  printf("destructorn");

  throw 0;

  }

  void *operator new(size_t n) throw()

  {

  printf("newn");

  return malloc(n);

  }

  void operator delete(void *p) throw()

  {

  printf("deleten");

  if (p != NULL)

  free(p);

  }

  };

 

int main()

  {

  X *x = new X;

  try

  {

  delete x;

  }

  catch (...)

  {

  printf("catchn");

  }

  return 0;

  }

 

  如果解構函式沒有拋異常,程式輸出:

new

destructor

delete

 

  實際上程式輸出:

new

destructor

catch

 

  operator delete沒有進入,x的記憶體空間沒有被釋放,程式有資源洩漏,the press hammers your product for eating memory, and you go back to flip burgers for a living。

  原則:異常安全要求你不能在解構函式中拋異常。和在建構函式拋異常上有不同意見不一樣,這條是絕對的。為了明確表明意圖,應該在申明解構函式時加上異常規格申明throw()。

 

1.5  預告

  我本準備覆蓋模板安全的,但沒地方了。我將留到下次介紹,並開出推薦讀物表。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10748419/viewspace-1008584/,如需轉載,請註明出處,否則將追究法律責任。

相關文章