void f() throw(int, MyException); 看似完美:文檔化函數可能拋出的異常、編譯期部分檢查、執行期違規觸發 unexpected()。但深層陷阱眾多,需極度謹慎使用。
std::unexpected()。unexpected 預設呼叫 std::terminate()。terminate 預設呼叫 abort() — 立即終止程式,不解構區域物件,不清理資源。
extern void f1(); // 無規格,可拋出任何異常
void f2() throw(int) { // 規定只拋 int
f1(); // 合法!編譯器不檢查 f1 可能拋出非 int
}
標準禁止編譯器拒絕此類調用 (僅能警告)。新舊代碼整合時雖需此彈性,但極易埋下隱患。
template
bool operator==(const T& lhs, const T& rhs) throw() { // 宣稱不拋出
return &lhs == &rhs;
}
若 T 重載 operator& 且會拋出異常 → 違反 throw() → 呼叫 unexpected。
模板幾乎無法提供有意義的異常規格,因不知型別參數會做何操作。原則:模板內避免異常規格。
typedef void (*Callback)(int, int, void*);
class System {
Callback cb;
public:
void trigger() throw() { cb(x, y, data); } // 危險!cb 可能拋出
};
解決:在函數指標型別加規格 typedef void (*Callback)(...) throw();,強制回調不拋出 (需編譯器支援)。否則移除觸發函數的異常規格。
任何 new 可能拋出 std::bad_alloc。若函數規格未含它,配置失敗即觸發 unexpected。
幾乎所有配置記憶體的函數都不應有限制性異常規格。
若無法預防違規 (如必須呼叫無規格舊庫),可替換 unexpected 行為:
class UnexpectedException {}; // 統一轉換型別
void convertUnexpected() { throw UnexpectedException(); }
std::set_unexpected(convertUnexpected); // 安裝
// 或保留原始異常類型 (轉為 bad_exception)
void convertUnexpected() { throw; } // 重新拋出當前異常
std::set_unexpected(convertUnexpected);
// 需在所有規格含 bad_exception 或基類 exception
違規異常將被轉換為指定型別繼續傳播,避免 terminate。
class Session {
static void logDestruction(Session*) throw(); // 宣稱不拋出
public:
~Session() {
try { logDestruction(this); }
catch (...) { } // 想處理所有異常
}
};
若 logDestruction 內部違規拋出 → unexpected → terminate → 析構函數的 catch 永無機會執行。異常規格反而阻礙了正確的錯誤處理。
| 場景 | 建議 |
|---|---|
| Leaf 函數 (不呼叫其他可拋出代碼)、明確不拋出 | 可用 throw() (C++11 noexcept) |
| 模板函數 | 絕不使用 |
| 呼叫無規格/第三方代碼 | 不要加規格 或移除既有規格 |
| 配置記憶體 (new) | 不要限制規格 (含 bad_alloc) |
| 虛擬函數介面 | 謹慎:衍生類覆寫需遵守基類規格 (嚴格) |
| 需要文檔化異常 | 改用註解 // Throws: MyException, std::bad_alloc |
異常規格是「帶刺的玫瑰」:文檔價值有限,執行風險極高,編譯器檢查又極弱。現代 C++ (C++11 起) 以
noexcept取代,語意更清晰 (是否拋出而非列舉型別),優化機會更大。除非極度確定,否則寧缺勿濫。