錯誤與例外處理
參考資料
以下文件很好地介紹了撰寫健全泛型組件的一些議題
D. Abrahams:「泛型組件中的例外安全性」,最初發表於 M. Jazayeri、R. Loos、D. Musser(編):泛型程式設計,達格斯圖研討會論文集,電腦科學講義。第 1766 卷
指導方針
我應該何時使用例外?
簡單的答案是:「只要例外的語義和效能特性合適時」。
一個常被引用的指導方針是問自己:「這是一個例外(或非預期)的情況嗎?」 這個指導方針聽起來很有吸引力,但通常是錯誤的。問題在於,一個人的「例外」是另一個人的「預期」:仔細研究這些術語後,區別就消失了,你也就沒有指導方針了。畢竟,如果你檢查錯誤條件,那麼在某種意義上,你預期它會發生,否則檢查就是浪費程式碼。
更適當的問題是:「我們這裡需要堆疊回溯嗎?」 因為實際處理例外可能比執行主線程式碼慢得多,你也應該問:「我這裡可以負擔堆疊回溯的成本嗎?」 例如,執行長時間計算的桌面應用程式可能會定期檢查使用者是否按下了取消按鈕。 拋出例外可以允許操作正常取消。 另一方面,在這個計算的內迴圈中拋出並*處理*例外可能是不合適的,因為這可能會對效能產生重大影響。 上面提到的指導方針有一點道理:在時間緊迫的程式碼中,拋出例外應該是*例外*,而不是規則。
我應該如何設計我的例外類別?
- 從
std::exception
衍生你的例外類別。 除非在*非常*罕見的情況下,你無法負擔虛擬表格的成本,否則std::exception
是一個合理的例外基底類別,並且在普遍使用時,允許程式設計師捕捉「所有」例外,而無需使用catch(...)
。 有關catch(...)
的更多資訊,請參見下文。 -
使用*虛擬*繼承。 這個見解來自 Andrew Koenig。 從你的例外基底類別使用虛擬繼承可以防止在捕捉點發生歧義問題,以防有人拋出從多個具有共同基底類別的基底類別衍生的例外。
#include <iostream> struct my_exc1 : std::exception { char const* what() const throw(); }; struct my_exc2 : std::exception { char const* what() const throw(); }; struct your_exc3 : my_exc1, my_exc2 {}; int main() { try { throw your_exc3(); } catch(std::exception const& e) {} catch(...) { std::cout << "whoops!" << std::endl; } }
上面的程式會印出「whoops」,因為 C++ 執行階段無法解析第一個 catch 子句中要匹配的exception
實例。 -
*不要*嵌入
std::string
物件 或任何其他其複製建構函式可能會拋出例外的資料成員或基底類別。 這可能會直接導致在拋出點發生std::terminate()
。 同樣,使用其一般建構函式可能會拋出例外的基底類別或成員也是一個壞主意,因為雖然不一定會對你的程式造成致命影響,但你可能會從包含建構的 *throw-expression* 中回報與預期不同的例外,例如throw some_exception();
有許多方法可以避免在複製異常時拷貝字串物件,包括在異常物件中嵌入固定長度的緩衝區,或透過參考計數來管理字串。然而,在採用這些方法之前,請考慮以下幾點。
- **視需要格式化 `what()` 訊息**,如果您真的必須格式化訊息的話。格式化異常錯誤訊息通常是一個記憶體密集的操作,而且有可能拋出異常。最好將此操作延遲到堆疊回溯完成之後,並且假設已釋放一些資源。在這種情況下,最好用 `catch(...)` 區塊保護您的 `what()` 函式,以便在格式化程式碼拋出異常時有備用方案。
- **不要*過度*擔心 `what()` 訊息**。雖然能讓程式設計師有機會理解的訊息很好,但在拋出異常時,您不太可能撰寫出使用者也能理解的相關錯誤訊息。當然,國際化也超出異常類別作者的職責範圍。 Peter Dimov 提出了一个很好的論點,`what()` 字串的正確用法是作為錯誤訊息格式化器表格的鍵值。現在,如果我們能針對標準函式庫拋出的異常取得標準化的 `what()` 字串就好了...
- **在異常類別的公開介面中揭露與錯誤原因相關的資訊**。過度關注 `what()` 訊息可能會導致您忽略揭露其他人可能需要的資訊,以便為使用者建立一致的訊息。例如,如果您的異常回報數值範圍錯誤,那麼在異常類別的公開介面中以*數字*形式提供相關的實際數字非常重要,以便錯誤回報程式碼可以對其進行一些智慧處理。如果您只在 `what()` 字串中揭露這些數字的文字表示,那麼對於需要對這些數字進行更多操作(例如減法)而不是僅僅輸出它們的程式設計師來說,將會非常困難。
- **盡可能使您的異常類別免於重複解構**。不幸的是,一些常用的編譯器偶爾會導致異常物件被解構兩次。如果您可以將其設定為無害(例如,將已刪除的指標歸零),您的程式碼將會更健壯。
程式設計師錯誤怎麼辦?
作為開發人員,如果我違反了我正在使用的函式庫的前提條件,我不希望堆疊回溯。我想要的是核心傾印或等效的東西——一種在檢測到問題的確切位置檢查程式狀態的方法。這通常意味著使用 `assert()` 或類似的方法。
有時需要具有彈性的 API,可以承受幾乎任何類型的客戶端濫用,但這種方法通常需要付出相當大的代價。例如,它通常需要追蹤客戶端使用的每個物件,以便檢查其有效性。如果您需要這種保護,通常可以將其作為一個層建構在更簡單的 API 之上。但是,要小心半調子措施。承諾可以抵禦某些但並非所有濫用的 API 會招致災難。客戶端將開始依賴這種保護,並且他們的期望將會擴展到涵蓋未受保護的介面部分。
Windows 開發者注意事項:很遺憾的是,大多數 Windows 編譯器使用的原生異常處理機制在使用 assert()
時實際上會拋出異常。事實上,其他程式設計師錯誤,例如區段錯誤和除零錯誤,也是如此。這樣做的一個問題是,如果您使用 JIT(即時)除錯,在除錯器啟動之前會發生額外的異常回溯,因為 catch(...)
會捕捉到這些並非真正的 C++ 異常。幸運的是,有一個簡單但鮮為人知的解決方法,就是使用以下咒語
extern "C" void straight_to_debugger(unsigned int, EXCEPTION_POINTERS*) { throw; } extern "C" void (*old_translator)(unsigned, EXCEPTION_POINTERS*) = _set_se_translator(straight_to_debugger);
如果 SEH 是從 catch 區塊內(或從 catch 區塊內呼叫的函式)引發的,則此技術無效,但它仍然消除了絕大多數 JIT 遮蔽問題。
我應該如何處理異常?
通常,處理異常的最佳方法是根本不處理它們。如果您可以讓它們通過您的程式碼,並允許解構子處理清理工作,您的程式碼會更簡潔。
盡可能避免使用 catch(...)
遺憾的是,Windows 以外的其他作業系統也會將非 C++「異常」(例如執行緒取消)納入 C++ EH 機制中,而且有時沒有對應於上述 _set_se_translator
技巧的解決方法。結果是,catch(...)
可能會在無法復原的地方產生一些意外的系統通知,看起來就像從合理的地方拋出的 C++ 異常,從而使解構子和 catch 區塊在回溯期間已採取有效步驟來確保程式不變量的通常安全假設失效。在新聞群組中經過多次長時間的辯論後,我不情願地向 Hillel Y. Sims 承認這一點:在所有作業系統都被「修復」之前,如果每個異常都衍生自 std::exception
,並且每個人都用 catch(std::exception&)
替換 catch(...)
,世界會變得更美好。
儘管與作業系統/平台設計選擇存在不良互動,但有時 catch(...)
仍然是最合適的模式。如果您不知道可能會拋出哪種異常,並且您真的*必須*停止回溯,它可能仍然是您的最佳選擇。語言邊界就是一個明顯的例子。