More effective C++ 條款14 (轉)

gugu99發表於2008-07-25
More effective C++ 條款14 (轉)[@more@] 

條款14:審慎使用異常規格(exception specifications):namespace prefix = o ns = "urn:schemas--com::office" />

毫無疑問,異常規格是一個引人注目的特性。它使得程式碼更容易理解,因為它明確地描述了一個可以丟擲什麼樣的異常。但是它不只是一個有趣的註釋。在編譯時有時能夠檢測到異常規格的不一致。而且如果一個函式丟擲一個不在異常規格範圍裡的異常,在執行時能夠檢測出這個錯誤,然後一個特殊函式unexpected將被自動地。異常規格既可以做為一個指導性文件同時也是異常使用的強制機制,它好像有著很誘人的外表。

不過在通常情況下,美貌只是一層皮,外表的美麗並不代表其內在的素質。函式unexpected預設的行為是呼叫函式tenate,而terminate預設的行為是呼叫函式abort,所以一個違反異常規格的其預設的行為就是halt(停止執行)。在啟用的stack frame中的區域性變數沒有被釋放,因為abort在關閉程式時不進行這樣的清除操作。對異常規格的觸犯變成了一場並不應該發生的災難。

不幸的是,我們很容易就能夠編寫出導致發生這種災難的函式。編譯器僅僅部分地檢測異常的使用是否與異常規格保持一致。一個函式呼叫了另一個函式,並且後者可能丟擲一個違反前者異常規格的異常,(A函式呼叫B函式,因為B函式可能丟擲一個不在A函式異常規格之內的異常,所以這個函式呼叫就違反了A函式的異常規格  譯者注)編譯器不對此種情況進行檢測,並且語言標準也禁止它們拒絕這種呼叫方式(儘管可以顯示警告資訊)。

例如函式f1沒有宣告異常規格,這樣的函式就可以丟擲任意種類的異常:

extern void f1();  // 可以丟擲任意的異常

假設有一個函式f2透過它的異常規格來宣告其只能丟擲int型別的異常:

void f2() throw(int);


f2呼叫f1是非常合法的,即使f1可能丟擲一個違反f2異常規格的異常:

void f2() throw(int)


{


  ...


  f1();  // 即使f1可能丟擲不是int型別的


  //異常,這也是合法的。


  ...


}


當帶有異常規格的新程式碼與沒有異常規格的老程式碼整合在一起工作時,這種靈活性就顯得很重要。

因為你的編譯器允許你呼叫一個函式其丟擲的異常與發出呼叫的函式的異常規格不一致,並且這樣的呼叫可能導致你的程式被終止,所以在編寫時採取措施把這種不一致減小到最少。一種好方法是避免在帶有型別引數的模板內使用異常規格。例如下面這種模板,它好像不能丟擲任何異常:

// a poorly designed template wrt exception specifications


template


bool operator==(const T& lhs, const T& rhs) throw()


{


  return &lhs == &rhs;


}


這個模板為所有型別定義了一個運算子函式operator==。對於任意一對型別相同的,如果物件有一樣的地址,該函式返回true,否則返回false。

這個模板包含的異常規格表示模板生成的函式不能丟擲異常。但是事實可能不會這樣,因為opertor&(地址運算子,參見Effective C++ 條款45)能被一些型別物件過載。如果被過載的話,當呼叫從operator==函式內部呼叫opertor&時,opertor&可能會丟擲一個異常,這樣就違反了我們的異常規格,使得程式控制跳轉到unexpected。

上述的例子是一種更一般問題的特例,這個問題也就是沒有辦法知道某種模板型別引數丟擲什麼樣的異常。我們幾乎不可能為一個模板提供一個有意義的異常規格。,因為模板總是採用不同的方法使用型別引數。解決方法只能是模板和異常規格不要混合使用。

能夠避免呼叫unexpected函式的第二個方法是如果在一個函式內呼叫其它沒有異常規格的函式時應該去除這個函式的異常規格。這很容易理解,但是實際中容易被忽略。比如允許註冊一個回撥函式:

// 一個window系統回撥函式指標


//當一個window系統事件發生時


typedef void (*CallBackPtr)(int eventXLocation,


  int eventYLocation,


  void *dataToPassBack);


//window系統類,含有回撥函式指標,


//該回撥函式能被window系統客戶註冊


class CallBack {


public:


  CallBack(CallBackPtr fPtr, void *dataToPassBack)


  : func(fPtr), data(dataToPassBack) {}


  void makeCallBack(int eventXLocation,


  int eventYLocation) const throw();


private:


  CallBackPtr func;  // function to call when


  // callback is made


  void *data;  // data to pass to callback


};  // function


// 為了實現回撥函式,我們呼叫註冊函式,


//事件的作標與註冊資料做為函式引數。


void CallBack::makeCallBack(int eventXLocation,


  int eventYLocation) const throw()


{


  func(eventXLocation, eventYLocation, data);


}


這裡在makeCallBack內呼叫func,要冒違反異常規格的風險,因為無法知道func會丟擲什麼型別的異常。

透過在程式在CallBackPtr typedef中採用更嚴格的異常規格來解決問題:

typedef void (*CallBackPtr)(int eventXLocation,


  int eventYLocation,


  void *dataToPassBack) throw();


這樣定義typedef後,如果註冊一個可能會丟擲異常的callback函式將是的:

// 一個沒有異常給各的回撥函式


void callBackFcn1(int eventXLocation, int eventYLocation,


  void *dataToPassBack);


void *callBackData;


...


CallBack c1(callBackFcn1, callBackData);


  //錯誤!callBackFcn1可能


  // 丟擲異常


//帶有異常規格的回撥函式


void callBackFcn2(int eventXLocation,


  int eventYLocation,


  void *dataToPassBack) throw();


CallBack c2(callBackFcn2, callBackData);


  // 正確,callBackFcn2


   // 沒有異常規格


傳遞函式指標時進行這種異常規格的檢查,是語言的較新的特性,所以有可能你的編譯器不支援這個特性。如果它們不支援,那就依靠你自己來確保不能犯這種錯誤。

避免呼叫unexpected的第三個方法是處理系統本身丟擲的異常。這些異常中最常見的是bad_alloc,當分配失敗時它被operator newoperator new[]丟擲(參見條款8)。如果你在函式里使用new運算子(還參見條款8),你必須為函式可能遇到bad_alloc異常作好準備。

現在常說預防勝於治療(即做任何事都要未雨綢繆  譯者注),但是有時卻是預防困難而治療容易。也就是說有時直接處理unexpected異常比防止它們被丟擲要簡單。例如你正在編寫一個軟體,精確地使用了異常規格,但是你必須從沒有使用異常規格的程式庫中呼叫函式,要防止丟擲unexpected異常是不現實的,因為這需要改變程式庫中的程式碼。

雖然防止丟擲unexpected異常是不現實的,但是C++允許你用其它不同的異常型別替換unexpected異常,你能夠利用這個特性。例如你希望所有的unexpected異常都被替換為UnexpectedException物件。你能這樣編寫程式碼:

class UnexpectedException {};  // 所有的unexpected異常物件被


  //替換為這種型別物件


 


 


void convertUnexpected()  // 如果一個unexpected異常被


{  // 丟擲,這個函式被呼叫


  throw UnexpectedException(); 


}


透過用convertUnexpected函式替換預設的unexpected函式,來使上述程式碼開始執行。:

set_unexpected(convertUnexpected);


當你這麼做了以後,一個unexpected異常將觸發呼叫convertUnexpected函式。Unexpected異常被一種UnexpectedException新異常型別替換。如果被違反的異常規格包含UnexpectedException異常,那麼異常傳遞將繼續下去,好像異常規格總是得到滿足。(如果異常規格沒有包含UnexpectedException,terminate將被呼叫,就好像你沒有替換unexpected一樣)

另一種把unexpected異常轉變成知名型別的方法是替換unexpected函式,讓其重新丟擲當前異常,這樣異常將被替換為bad_exception。你可以這樣編寫:

void convertUnexpected()  // 如果一個unexpected異常被


{  //丟擲,這個函式被呼叫


  throw;  // 它只是重新丟擲當前


}  // 異常


 


set_unexpected(convertUnexpected);


  // convertUnexpected


  // 做為unexpected


   // 的替代品


如果這麼做,你應該在所有的異常規格里包含bad_exception(或它的基類,標準類exception)。你將不必再擔心如果遇到unexpected異常會導致程式執行終止。任何不聽話的異常都將被替換為bad_exception,這個異常代替原來的異常繼續傳遞。

到現在你應該理解異常規格能導致大量的麻煩。編譯器僅僅能部分地檢測它們的使用是否一致,在模板中使用它們會有問題,一不注意它們就很容易被違反,並且在預設的情況下它們被違反時會導致程式終止執行。異常規格還有一個缺點就是它們能導致unexpected被觸發即使一個high-level呼叫者準備處理被丟擲的異常,比如下面這個幾乎一字不差地來自從條款11例子:

class Session {  // for modeling online


public:  // sessions


  ~Session();


  ...


 


private:


  static void logDestruction(Session *objAddr) throw();


};


 


Session::~Session()


{


  try {


  logDestruction(this);


  }


  catch (...) {  }


}

session的解構函式呼叫logDestruction記錄有關session物件被釋放的資訊,它明確地要捕獲從logDestruction丟擲的所有異常。但是logDestruction的異常規格表示其不丟擲任何異常。現在假設被logDestruction呼叫的函式丟擲了一個異常,而logDestruction沒有捕獲。我們不會期望發生這樣的事情,凡是正如我們所見,很容易就會寫出違反異常規格的程式碼。當這個異常透過logDestruction傳遞出來,unexpected將被呼叫,預設情況下將導致程式終止執行。這是一個正確的行為,這是session解構函式的作者所希望的行為麼?作者想處理所有可能的異常,所以好像不應該不給session解構函式里的catch塊執行的機會就終止程式。如果logDestruction沒有異常規格,這種事情就不會發生。(一種防止的方法是如上所描述的那樣替換unexpected)

以全面的角度去看待異常規格是非常重要的。它們提供了優秀的文件來說明一個函式丟擲異常的種類,並且在違反它的情況下,會有可怕的結果,程式被立即終止,在預設時它們會這麼做。同時編譯器只會部分地檢測它們的一致性,所以他們很容易被不經意地違反。而且他們會阻止high-level異常來處理unexpected異常,即使這些異常處理器知道如何去做。綜上所述,異常規格是一個應被審慎使用的公族。在把它們加入到你的函式之前,應考慮它們所帶來的行為是否就是你所希望的行為。


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

相關文章