程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> More effective C++:審慎使用異常規格

More effective C++:審慎使用異常規格

編輯:關於C++

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

不過在通常情況下,美貌只是一層皮,外表的美麗並不代表其內在的素質。函數unexpected缺省的行為是調用函數terminate,而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<class T>
bool operator==(const T& lhs, const T& rhs) throw()
{
 return &lhs == &rhs;
}

這個模板為所有類型定義了一個操作符函數operator==。對於任意一對類型相同的對象,如果對象有一樣的地址,該函數返回true,否則返回false。

這個模板包含的異常規格表示模板生成的函數不能拋出異常。但是事實可能不會這樣,因為opertor&能被一些類型對象重載。如果被重載的話,當調用從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[]拋出(參見條款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異常,即使這些異常處理器知道如何去做。綜上所述,異常規格是一個應被審慎使用的公族。在把它們加入到你的函數之前,應考慮它們所帶來的行為是否就是你所希望的行為。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved