異常規格(throw())看似吸引人:它文檔化了函數可能拋出的異常,編譯器能部分檢查一致性,運行時違反會調用 unexpected()。但實際上它充滿陷阱,C++11 已將其棄用(改用 noexcept),現代 C++ 開發強烈建議避免使用。
unexpected() 預設調用 terminate()。terminate() 預設調用 abort()。abort() 直接終止程序,不調用析構函數,棧上對象資源洩漏。一個違反異常規格的錯誤會導致程序立即崩潰,無法進行任何資源清理。
編譯器不檢查函數調用鏈中的異常規格一致性:
extern void f1(); // 無異常規格,可拋出任意異常
void f2() throw(int) { // 承諾只拋 int
f1(); // 合法!即使 f1 拋出 double 違反 f2 規格
}
這種「靈活性」為與舊代碼兼容而設計,實則埋下隱患。
模板類型參數的異常行為未知,無法提供有意義的異常規格:
template<class T>
bool operator==(const T& lhs, const T& rhs) throw() // 承諾不拋異常
{
return &lhs == &rhs; // 若 T 重載 operator& 並拋異常,違反規格
}
結論:模板與異常規格不可混用。
若類接受用戶回調函數指針,無法知曉其異常行為:
typedef void (*CallBackPtr)(int, int, void*);
class CallBack {
void makeCallBack(int x, int y) const throw() {
func(x, y, data); // 危險:func 可能拋異常
}
CallBackPtr func;
void *data;
};
解決:在函數指針類型中加入異常規格:
typedef void (*CallBackPtr)(int, int, void*) throw(); // 強制回調不拋異常
但此特性需編譯器支持(較新標準)。
使用 new 可能拋出 std::bad_alloc,若函數有 throw() 規範會導致 unexpected。必須捕獲或放寬規格。
若無法預防(如調用無異常規格的第三方庫),可替換 unexpected 行為:
class UnexpectedException {};
void convertUnexpected() { throw UnexpectedException(); }
set_unexpected(convertUnexpected); // 安裝處理器
若違反的異常規格包含 UnexpectedException,傳播繼續;否則調用 terminate。
void convertUnexpected() { throw; } // 重新拋出
set_unexpected(convertUnexpected);
所有違規異常統一替換為 std::bad_exception(需在異常規格中包含它或基類 std::exception),程序不再因 unexpected 終止。
即使調用者準備好處理具體異常,unexpected 也會優先介入導致程序終止:
class Session {
static void logDestruction(Session*) throw(); // 承諾不拋異常
~Session() {
try { logDestruction(this); }
catch (...) { } // 想捕獲所有異常
}
};
若 logDestruction 內部調用違反規格拋出異常,unexpected 直接終止程序,析構函數的 catch 永無機會執行。
| 問題 | 影響 |
|---|---|
| 編譯器僅部分檢查一致性 | 極易在不知情下違反 |
| 模板中無法編寫有意義規格 | 泛型代碼無法使用 |
| 違反時預設終止程序 | 資源洩漏,無法優雅降級 |
| 阻攔高層異常處理器 | 破壞異常傳播機制 |
noexcept (C++11):僅聲明「不拋異常」或「可能拋異常」,無運行時檢查開銷,違反直接 terminate(無 unexpected 環節),編譯器可優化。catch(...)),轉換為內部異常類型。異常規格是一個「看似美好實則危險」的特性,現代 C++ 開發應完全避免,改用 noexcept 配合良好的異常安全設計。