本文是做者翻譯過C++之父Bjarne Stroustrup的技術文章C++核心準則中有關C++中異常的文章以後的總結,但願讀者經過本文能夠對C++異常有一個全面,快速的瞭解:
git
異常處理機制但願解決的問題github
爲了使用錯誤處理系統化,健壯和不繁瑣。例以下面的代碼:web
void f2(int i) // Clumsy and error-prone: explicit release
{
int* p = new int[12];
// ...
if (i < 17) {
delete[] p;
throw Bad{"in f()", i};
}
// ...
}
代碼中的做者須要針對每種錯誤進行處理,更爲複雜的是當程序的規模達到必定程度以後,在各個模塊之間和調用的各個層級之間傳遞錯誤信息也會變成一個巨大的負擔。異常就是爲了解決這個問題而出現的。
編程
推薦使用異常的狀況設計模式
通常狀況下會認爲異常意味着重大的例外事件和錯誤。例以下面的狀況:
微信
一個前提條件沒有知足網絡
構造函數沒法構造對象(沒法創建類的不變式)架構
越界錯誤(例如 v[v.size()]=7)app
沒法獲取資源(例如:網絡斷)ide
經過拋出異常來向調用者代表函數沒法執行指定的任務。
不該該使用異常的狀況
循環的正常終止,處理的正常結束都是正常和期待的動做,不該該被視爲異常。這種作法能夠保證錯誤處理和「普通的代碼」分離。C++編譯器會以異常處理很罕見爲前提進行代碼優化。不要使用將拋出異常做爲從函數中返回結果的另外一種方式使用。
使用異常時應防止資源泄露
資源泄露一般都是不可接受的。若是隻是簡單的去掉原有的錯誤處理代碼並增長異常拋出和處理代碼,一般會發生資源泄露。例以下面的代碼:
void leak(int x) // don't: may leak{ auto p = new int{7}; if (x < 0) throw Get_me_out_of_here{}; // may leak *p // ... delete p; // we may never get here}
手動釋放資源雖然不是徹底作不到,可是工做量巨大且容易引起錯誤。
void f2(int i) // Clumsy and error-prone: explicit release
{
int* p = new int[12];
// ...
if (i < 17) {
delete[] p;
throw Bad{"in f()", i};
}
// ...
}
這樣的代碼過於冗長,甚至比不用異常的代碼更加冗長。在更大規模的,存在更多的拋出異常的可能性的代碼中,顯式釋放資源會更加繁複和易錯。解決這個問題的方法是RAII(「資源請求即初始化」),它是防止泄露最簡單,更加系統化的方式。
void f3(int i) // OK: resource management done by a handle (but see below)
{
auto p = make_unique<int[]>(12);
// ...
if (i < 17) throw Bad{"in f()", i};
// ...
}
另一個解決方案(一般更好)是用局部變量來避免使用指針。
void no_leak_simplified(int x){ vector<int> v(7); // ...}
定義和使用本身的異常類型
使用用戶定義類型的好處是幾乎和其餘人的異常發生衝突。這種問題在代碼規模變大以後會在不知不覺出現。繼承自exception的標準庫類應該只用於基類或只要求「一般」處理的異常。和內置類型類似,對它們的使用也有可能和其餘人的使用發生衝突。
使用常量引用形式捕捉繼承體系中的異常
爲了不數據截斷。大多數處理程序不會改變異常的內容,所以一般咱們同時推薦使用常量形式。
正確排列catch子句
catch子句按照它們表示的次序行,一個子句處理以後,其餘子句再也不執行。
void f()
{
// ...
try {
// ...
}
catch (Base& b) { /* ... */ }
catch (Derived& d) { /* ... */ }
catch (...) { /* ... */ }
catch (std::exception& e) { /* ... */ }
}
若是Deriveds是Base的派生類,捕捉派生類的處理永遠不會執行。捕捉全部異常的處理會致使捕捉std::exception的處理程序永遠不會執行。
從新拋出異常
從新拋出已經捕獲的異常時必定要使用throw;而不是throw e;。使用後者會拋出一個e的新拷貝(靜態類型std::exception的截斷結果)而不是從新拋出原始異常。
關於noexcept
爲了讓錯誤處理更系統化,健壯和高效能夠爲函數定義noexcept。由於某段代碼有不會拋出異常的操做構成,因此咱們知道某函數不會拋出異常。經過將函數定義爲noexcept,我向編譯器和代碼的讀者傳遞了可讓它們更容易理解和維護的信息。不少標準庫函數被定義爲noexcept,包含全部從C標準庫繼承的標準庫函數。
但應該注意的是,一旦定義了noexcept,C++編譯器就會放棄爲函數生成接受、轉發異常的處理。若是實際發生了異常,結果是毀滅性的。必定要慎重定義noexcept。
析構函數,內存釋放和swap操做永遠不能失敗
若是析構函數、swap操做或者內存釋放失敗了,咱們不知道如何編寫可信賴的處理程序;也就是說,若是它由於異常退出或者只是沒有執行要求的操做。標準庫假設析構函數,內存釋放函數(例如delete運算符),swap都不會拋出異常。若是它們異常,標準庫的前提條件就被破壞了。
不要試圖在全部函數中捕捉全部異常
在一個沒法提供有意義的恢復操做的函數中捕捉錯誤會致使代碼複雜化和冗餘。讓異常向外傳播直到到達一個能夠處理它的函數。讓RAII處理調用路徑上的清理動做。
try/catch結構冗長,非平凡的用法容易出錯。try/catch能夠看做是非系統化和低層次資源管理或錯誤處理的信號。
最小限度顯式使用try/catch。
沒法使用異常的狀況
有些系統,例如硬實時系統要求保證一個動做在開始執行以前就能肯定其執行時間小於某個固定值(一般很小)。這樣的系統只有在存在某種能夠準確預測系統從拋出異常過程當中恢復的最大時間的工具時纔可使用異常。若是沒有適當的時間評價工具,異常處理機制很難知足這個要求。這樣的系統(例如飛行控制系統)一般也會禁止使用動態(堆)內存。
不要使用拋異常聲明
拋異常聲明原本的目的是明確代表某個函數可能拋出的異常。
int use(int arg)
throw(X, Y)
{
// ...
auto x = f(arg);
// ...
}
可是異常聲明讓錯誤處理更脆弱,並強制產生運行時成本,已經從C++標準中被移除了。在不會拋出任何異常時,使用noexcept或者和它等價的throw()是才更加正確的作法。
關於異常代價和性能
不少關於異常的大量恐懼都是被誤導的。當在沒有被指針或複雜的控制結構搞亂的代碼環境中使用異常時,異常處理幾乎老是能夠接受的(不管是時間仍是空間維度),幾乎老是能夠帶來更好的代碼。
在譴責異常或抱怨異常的成本太高以前,考慮使用錯誤代碼時的成本和複雜度。若是你擔憂性能,進行測量(而不是無根據的懷疑,譯者注)
參考資料
C++核心準則英文原文(C++之父Bjarne Stroustrup的技術文章):
https://github.com/isocpp/CppCoreGuidelines/blob/master/CppCoreGuidelines.md
異常相關翻譯文章彙總:
https://mp.weixin.qq.com/mp/appmsgalbum?action=getalbum&album_id=1575975158413099008&__biz=MzI2MDYyNDgzMQ==#wechat_redirect
更多中文翻譯請參考本人公衆號【面向對象思考】。
新書介紹
如下是本人最近出版的新書,拜託多多關注!
本書利用Python 的標準GUI 工具包tkinter,經過可執行的示例對23 個設計模式逐個進行說明。這樣一方面可使讀者瞭解真實的軟件開發工做中每一個設計模式的運用場景和想要解決的問題;另外一方面經過對這些問題的解決過程進行說明,讓讀者明白在編寫代碼時如何判斷使用設計模式的利弊,併合理運用設計模式。
對設計模式感興趣並且但願隨學隨用的讀者經過本書能夠快速跨越從理解到運用的門檻;但願學習Python GUI 編程的讀者能夠將本書中的示例做爲設計和開發的參考;使用Python 語言進行圖像分析、數據處理工做的讀者能夠直接以本書中的示例爲基礎,迅速構建本身的系統架構。
以爲本文有幫助?請分享給更多人。
關注微信公衆號【面向對象思考】輕鬆學習每一天!
面向對象開發,面向對象思考!
本文分享自微信公衆號 - 面向對象思考(OOThinkingDalian)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。