錯誤和異常處理參考
關於編寫健壯的萬用群組件的一些問題,下面的論文給出了很好的介紹:
D. Abrahams: ``Exception Safety in Generic Components'', 由M. Jazayeri, R. Loos, D. Musser (eds.): Generic Programming, Proc. of a Dagstuhl Seminar, Lecture Notes on Computer Science. Volume. 1766 出版
指南
何時使用異常?
一個簡單的回答是:“當異常的語義和效能要求都恰當的時候。”
一個經常被提到的方法是這樣問自己:“這是一個例外(或者意外的)情形嗎?”這個方法貌似挺吸引人,但是通常只會導致錯誤答案。對一個人來說是“異常”的情形對另一個人卻“正常”:當你真正仔細考慮這句話時,就發現無法作出區分,這句話根本幫不了你。畢竟,如果你檢查了某個錯誤條件,就意味著你認為它會發生,否則你的檢查不過是垃圾代碼。
一個更合適的問法是:“這裡需要棧展開嗎?”由於異常處理實際上幾乎都意味著比正常流程代碼要慢,還應該問自己:“這裡負擔得起棧展開的代價嗎?”比如,正在做的一個要花很長時間的計算,並且周期性地檢測使用者是否按下了取消鍵。拋出異常可以優雅地取消操作。另一方面,在這個計算的內部迴圈中拋出並捕獲處理異常可能就不恰當,這麼做可能導致嚴重的效能下降。前述內容包含這樣一個原則:對於時間關鍵的代碼,拋出異常才是一種“異常”的做法,而不是常規.
如何設計異常類?
1. 從std::exception派生異常類。除了一些非常罕見的情況,例如負擔不了需函數的開銷。把std::exception作為異常基類是合理的,當它被廣泛使用後,將允許程式員捕獲任何異常而不必使用catch(...).更多關於catch(...)的內容,請看後文。
2. 使用虛擬繼承。這個深刻的洞察力來自Andrew Koenig. 當拋出的一個異常是從多個基類派生,並且這些基類有共同的部分,catch點就會遇到歧義問題,從異常基類虛擬繼承可以防止這種歧義問題:
#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++運行時刻無法決定用那個exception執行個體去匹配第一個catch.(禿子:我的建議是這裡最好別使用多重繼承)
3. 不要內嵌std::string對象或者其他拷貝構造可能拋出異常的資料成員、基類。在上述點拋出異常將導致直接調用std::terminate().讓基類或資料成員的預設建構函式可能拋出異常也是同樣糟糕的主意,你本來是打算通過一個包含物件建構的throw運算式報告異常, 程式卻無謂地中止了:
throw some_exception();
當發生異常拷貝時,有幾種方法避免複製字串對象,例如在異常對象中嵌入一個定長儲存區,或者通過引用計數來管理字串。不過,在採用這些方法前,先考慮考慮下一條。
4. 只在確實需要的時候才格式化what()返回的資訊。格式化是一個典型的記憶體相關的操作,有可能拋出異常。最好把格式化延遲到棧展開之後,因為棧展開可能釋放某些資源。對what()函數用catch(...)塊加以保護是一個好主意,這樣你就可以在格式化拋出異常時有了一個退路。
5. 不要太在意what()的資訊。在異常拋出點,對程式員來說,這是給出錯誤資訊的好機會,但是你未必能夠把相關資訊組合成使用者可以理解的形式。國際化就一個典型的情況。Peter Dimov給出了良好建議:建一個錯誤資訊格式化的表格,把what()的字串作為這個表的鍵。當標準庫拋出異常時,如果我們只能獲得其標準的what()字串……
6. 在異常類的public介面中暴露導致錯誤的有關資訊。返回固定資訊的what()意味著你忽視了暴露資訊,而使用者可能需要提供相關資訊。例如,你的異常想報告數字範圍錯,報錯的代碼應該能夠透過異常的公用介面讓異常包含導致問題的那個變數值。如果你只是在what()中以文本方式表現這些數字,那些需要根據資訊做更多(或更少)處理的程式員日子將很難過。
7. 如果可能,讓你的異常類對兩次析構免疫。幾款流行的編譯器偶爾會使異常對象被銷毀兩次。如果你能採取措施防禦危害(比如,把釋放的指標置零)就可以使代碼更健壯。
如何處理常式員犯錯?
作為開發人員,如果我違反了所使用庫的某個前條件,我不希望棧展開。我希望的是core dump或者等價物—一個能精確地在問題發生點檢查程式狀態的方法。這通常意味著assert()或者其他類似的東西。
有時候為使用者提供可以應付任意誤用的強健的API是有必要的,但這樣通常要付出不菲的代價。比如,一個常見需求是跟蹤客戶使用的每一個對象,從而可以驗證合法性。如果你需要這種保護,通常是在一個簡單API上再封裝一層來實現。儘管你做得小心翼翼,有強健承諾的API也只能防禦某些而不是所有會導致災難的誤用。客戶也開始依賴那些保護並且所依賴的保護也將增長到介面保護不到的部分。
windows開發人員請注意:當你使用assert()時,大部分Windows編譯器實際上都是拋出異常,並且被本地截獲,這很不幸。事實上,截獲的錯誤經常是段訪問失敗或者除零錯。當你使用JIT(Just In Time)調試時這是個問題,這意味著在在喚醒調試器之前已經異常棧展開了,因為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);
這個方法無法應付在catch塊中(或者catch塊調用的函數中)拋出結構化異常的情況,但它確實可以解決絕大多數JIT導致的問題。
該如何處理異常?
壓根就不處理異常一般是處理異常的最好辦法。如果你讓異常穿越你的代碼,並且在解構函式中做清理工作,代碼會更乾淨。
儘可能避免catch(…)
很不幸,其他非Windows作業系統一樣會把非C++異常(例如線程中止)捲入到C++異常機制中去,而且,有時候也沒有類似上面提到的_set_se_translator這樣的hack手法加以解決。我們通常在解構函式或者catch塊中做合理操作來維持系統的不變式,這通常是安全的。然而catch(...)也會捕獲非預期的系統通知,這時是不可能像對待普通C++異常一樣來處理的,慣用的手法不再安全了。
經過新聞群組上長期的辯論之後,儘管不情願,我還是得承認Hillel Y. Sims觀點:除非所有作業系統修正前面的問題,否則,所有異常應該繼承自std::exception,當所有人適應catch(std::exception&)而不是catch(...)時,世界將會更加美好。
即使不考慮和作業系統間糟糕的互動情況,有時候,catch(...)仍然是最合適的選擇。如果你根本不知道會有什麼異常拋出,並且必須停止棧展開,這可能是你唯一出路。一個典型的情況就是跨語言的時候。
Copyright David Abrahams 2001-2003. All rights reserved.
原文連結