天天看點

More Effective C++之 Item M14:審慎使用異正常格(exception specifications

毫無疑問,異正常格是一個引人注目的特性。它使得代碼更容易了解,因為它明确地描述了一個函數可以抛出什麼樣的異常。但是它不隻是一個有趣的注釋。編譯器在編譯時有時能夠檢測到異正常格的不一緻。而且如果一個函數抛出一個不在異正常格範圍裡的異常,系統在運作時能夠檢測出這個錯誤,然後一個特殊函數unexpected将被自動地調用。異正常格既可以做為一個指導性文檔同時也是異常使用的強制限制機制,它好像有着很誘人的外表。

    不過在通常情況下,美貌隻是一層皮,外表的美麗并不代表其内在的素質。函數unexpected預設的行為是調用函數terminate,而terminate預設的行為是調用函數abort,是以一個違反異正常格的程式其預設的行為就是halt(停止運作)。在激活的棧中的局部變量沒有被釋放,因為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<class T>

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 new 和operator new[]抛出(參見條款M8)。如果你在函數裡使用new操作符(還參見條款M8),你必須為函數可能遇到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調用者準備處理被抛出的異常,比如下面這個幾乎一字不差地來自從條款M11例子:

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異常,即使這些異常處理器知道如何去做。

    綜上所述,異正常格是一個應被審慎使用的特性。在把它們加入到你的函數之前,應考慮它們所帶來的行為是否就是你所希望的行為。