異常設計假定
Bjarne Stroustrup 闡述異常設計基於四點假定:
- 主要用於**錯誤處理**而非控制流。
- 異常處理代碼**遠少於**正常函數定義。
- 異常拋出頻率**遠低於**函數調用。
- 異常純屬**語言級**概念,不依賴作業系統支援。
異常 vs 返回值:語義差異
- **返回值**:函數可能出錯,**調用者可選擇忽略**,程序繼續執行。
- **異常**:函數出錯時**強制調用者處理**(捕獲),否則程序終止(`std::terminate`)。
建議:若錯誤屬於「預期內可恢復」(如文件不存在),用返回值/錯誤碼;若屬於「嚴重違反前置條件/不變量」(如除零、合約失效),用異常。
為何無異常規範
早期提議 `void f() throw(E1, E2)` 標註拋出異常類型,但導致:
- 修改底層函數異常類型需連鎖修改上層聲明與實現,破壞模塊獨立編譯。
- 模板實例化時異常類型難以靜態確定。
最終 C++ 標準**保留但棄用**動態異常規範,C++11 引入 `noexcept` 僅表達「不拋出」承諾。
異常核心:資源管理與 RAII
異常拋出會跳過後續代碼,若資源(文件、鎖、內存)在拋出點後釋放則洩漏。**RAII(Resource Acquisition Is Initialization)**應運而生:
- 資源獲取即初始化(構造函數)。
- 資源釋放綁定對象生命週期(析構函數)。
- 棧展開時自動調用析構函數,**保證無洩漏**。
構造函數中拋出異常的爭議
- Stroustrup:異常是構造函數報告失敗**唯一**途徑(構造函數無返回值)。
- Meyers/Sutter:構造函數拋出異常可能導致**已分配內存洩漏**(`new T()` 中 `T` 構造拋出,`operator new` 分配的內存無人釋放)。建議用雙階段初始化(`create()` + `init()`)或工廠函數返回 `optional`/`expected`。
折中:構造函數內部使用 RAII 管理子資源,若自身拋出,外層 `new` 需配合智能指針或 `try-catch` 釋放。
無法恢復執行點
異常**不支援**從拋出點恢復繼續執行(resumption model)。歷史證明恢復模型極難正確實現(上下文保存、副作用回滾),C++ 採用**終止模型**(termination model):捕獲後只能修復狀態並重試或傳播。
現代最佳實踐
- **異常安全保證**:基本承諾(不洩漏、不破壞不變量)、強烈承諾(事務性回滾)、不拋出承諾(`noexcept`)。
- **標準異常體系**:繼承 `std::exception`、`std::runtime_error`、`std::logic_error` 等。
- **邊界處理**:模塊邊界捕獲所有異常轉為錯誤碼/日誌,避免跨模塊拋出(C 接口、插件、線程入口)。
- **性能**:正常路徑零開銷(零成本異常模型),拋出路徑較慢但罕見。