More effective C++ 條款13 (轉)

worldblog發表於2007-12-09
More effective C++ 條款13 (轉)[@more@] 

條款13:透過引用(reference)捕獲異常:namespace prefix = o ns = "urn:schemas--com::office" />

當你寫一個catch子句時,必須確定讓異常透過何種方式傳遞到catch子句裡。你可以有三個選擇:與你給傳遞引數一樣,透過指標(by pointer),透過傳值(by value)或透過引用(by reference)。

我們首先討論透過指標方式捕獲異常(catch by pointer)。從throw處傳遞一個異常到catch子句是一個緩慢的過程,在理論上這種方法的實現對於這個過程來說是最高的。因為在傳遞異常資訊時,只有採用透過指標丟擲異常的方法才能夠做到不複製(參見條款12),例如:

class exception { ... };  // 來自標準C++庫(STL)


  // 中的異常類層次


  // (參見條款12)


 


void someFunction()


{


  static exception ex;  // 異常物件


 


...


 


 


  throw &ex;  // 丟擲一個指標,指向ex


 


  ...


 


}


 


void doSomething()


{


 try {


  someFunction();  // 丟擲一個 exception*


  }


  catch (exception *ex) {  // 捕獲 exception*;


  ...  // 沒有物件被複製


  }


}


這看上去很不錯,但是實際情況卻不是這樣。為了能讓正常執行,程式設計師定義異常物件時必須確保當程式控制權離開丟擲指標的函式後,物件還能夠繼續生存。全域性與靜態物件都能夠做到這一點,但是程式設計師很容易忘記這個。如果真是如此的話,他們會這樣寫程式碼:

void someFunction()


{


  exception ex;  // 區域性異常物件;


  // 當退出函式的生存空間時


  // 這個物件將被釋放。


  ... 


 


 


  throw &ex;  // 丟擲一個指標,指向


 ...  // 已被釋放的物件



這簡直糟糕透了,因為處理這個異常的catch子句接受到的指標,其指向的物件已經不再存在。

另一種丟擲指標的方法是在建立一個堆物件(new heap ):

void someFunction()


{


  ...


  throw new exception;  // 丟擲一個指標,指向一個在堆中


  ...  // 建立的物件(希望


}  // 運算子new — 參見條款8—


  // 自己不要再丟擲一個


  // 異常!)


這避免了捕獲一個指向已被釋放物件的指標的問題,但是catch子句的作者又面臨一個令人頭疼的問題:他們是否應該刪除他們接受的指標?如果是在堆中建立的異常物件,那他們必須刪除它,否則會造成資源洩漏。如果不是在堆中建立的異常物件,他們絕對不能刪除它,否則程式的行為將不可預測。該如何做呢?

這是不可能知道的。一些clients可能會傳遞全域性或靜態物件的地址,另一些可能轉遞堆中建立的異常物件的地址。透過指標捕獲異常,將遇到一個哈姆雷特式的難題:是刪除還是不刪除?這是一個難以回答的問題。所以你最好避開它。

而且,透過指標捕獲異常也不符合C++語言本身的規範。四個標準的異常――bad_alloc(當operator new(參見條款8)不能分配足夠的時,被丟擲),bad_cast(當dynamic_cast針對一個引用(reference)操作失敗時,被丟擲),bad_typeid(當dynamic_cast對空指標進行操作時,被丟擲)和bad_exception(用於unexpected異常;參見條款14)――都不是指向物件的指標,所以你必須透過值或引用來捕獲它們。

透過值捕獲異常(catch-by-value)可以解決上述的問題,例如異常物件刪除的問題和使用標準異常型別的問題。但是當它們被丟擲時將對異常物件複製兩次(參見條款12)。而且它會產生slicing problem,即派生類的異常物件被做為基類異常物件捕獲時,那它的派生類行為就被切掉了(sliced off)。這樣的sliced物件實際上是一個基類物件:它們沒有派生類的資料成員,而且當它們的虛擬函式時,系統解析後呼叫的是基類物件的函式。(當一個物件透過傳值方式傳遞給函式,也會發生一樣的情況――參見Effective C++ 條款22)。例如下面這個程式採用了擴充套件自標準異常類的異常類層次體系:

class exception {  // 如上,這是


public:  // 一個標準異常類


 


virtual const char * what() throw();


  // 返回異常的簡短描述.


  ...  // (在函式宣告的結尾處


  // 的"throw()",


};  //有關它的資訊


   // 參見條款14)


 


class runtime_error:  //也來自標準C++異常類


  public exception { ... }; 


 


class Validation_error:  // 客戶自己加入個類


  public runtime_error { 


public:


  virtual const char * what() throw();


   // 重新定義在異常類中


  ...  //虛擬函式


};  //


 


 


void someFunction()  // 丟擲一個 validation


{  // 異常


  ...


 


  if (a validation 測試失敗) {


  throw Validation_error();


  }


 


...


 


}


 


void doSomething()


{


  try {


  someFunction();  // 丟擲 validation


  }  //異常


 


 


  catch (exception ex) {  //捕獲所有標準異常類


  // 或它的派生類


 


 



   cerr << ex.what();  // 呼叫 exception::what(),


  ...  // 而不是Validation_error::what()



}


呼叫的是基類的what函式,即使被丟擲的異常物件是Validation_error Validation_error型別,它們已經重新定義的虛擬函式。這種slicing行為絕不是你所期望的。

最後剩下方法就是透過引用捕獲異常(catch-by-reference)。透過引用捕獲異常能使你避開上述所有問題。不象透過指標捕獲異常,這種方法不會有物件刪除的問題而且也能捕獲標準異常型別。也不象透過值捕獲異常,這種方法沒有slicing problem,而且異常物件只被複製一次。

我們採用透過引用捕獲異常的方法重寫最後那個例子,如下所示:

void someFunction()  //這個函式沒有改變



 ...


 


  if (a validation 測試失敗) {


  throw Validation_error();


  }


 


  ...


 


}


 


void doSomething()


{


  try {


  someFunction();  // 沒有改變


  }


  catch (exception& ex) {  // 這裡,我們透過引用捕獲異常


  // 以替代原來的透過值捕獲


 


 


  cerr << ex.what();  // 現在呼叫的是


   // Validation_error::what(),


  ...  // 而不是 exception::what()


  }


}


這裡沒有對throw進行任何改變,僅僅改變了catch子句,給它加了一個&符號。然而這個微小的改變能造成了巨大的變化,因為catch塊中的虛擬函式能夠如我們所願那樣工作了:呼叫的Validation_erro函式是我們重新定義過的函式。

如果你透過引用捕獲異常(catch by reference),你就能避開上述所有問題,不會為是否刪除異常物件而煩惱;能夠避開slicing異常物件;能夠捕獲標準異常型別;減少異常物件需要被複製的數目。所以你還在等什麼?透過引用捕獲異常吧(Catch exceptions by reference)


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

相關文章