一個簡單的回答是:“當(dāng)異常的語義和性能要求都恰當(dāng)?shù)臅r候。”
一個經(jīng)常被提到的方法是這樣問自己:“這是一個例外(或者意外的)情形嗎?”這個方法貌似挺吸引人,但是通常只會導(dǎo)致錯誤答案。對一個人來說是“異常”的情形對另一個人卻“正常”:當(dāng)你真正仔細(xì)考慮這句話時,就發(fā)現(xiàn)無法作出區(qū)分,這句話根本幫不了你。畢竟,如果你檢查了某個錯誤條件,就意味著你認(rèn)為它會發(fā)生,否則你的檢查不過是垃圾代碼。
一個更合適的問法是:“這里需要棧展開嗎?”由于異常處理實際上幾乎都意味著比正常流程代碼要慢,還應(yīng)該問自己:“這里負(fù)擔(dān)得起棧展開的代價嗎?”比如,正在做的一個要花很長時間的計算,并且周期性地檢測用戶是否按下了取消鍵。拋出異常可以優(yōu)雅地取消操作。另一方面,在這個計算的內(nèi)部循環(huán)中拋出并捕獲處理異常可能就不恰當(dāng),這么做可能導(dǎo)致嚴(yán)重的性能下降。前述內(nèi)容包含這樣一個原則:對于時間關(guān)鍵的代碼,拋出異常才是一種“異常”的做法,而不是常規(guī).
如何設(shè)計異常類?
1. 從std::exception派生異常類。除了一些非常罕見的情況,例如負(fù)擔(dān)不了需函數(shù)的開銷。把std::exception作為異常基類是合理的,當(dāng)它被廣泛使用后,將允許程序員捕獲任何異常而不必使用catch(...).更多關(guān)于catch(...)的內(nèi)容,請看后文。
2. 使用虛擬繼承。這個深刻的洞察力來自Andrew Koenig. 當(dāng)拋出的一個異常是從多個基類派生,并且這些基類有共同的部分,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; } } |
上面的程序?qū)⒋蛴〕觥皐hoops” ,因為C++運(yùn)行時刻無法決定用那個exception實例去匹配第一個catch.(禿子:我的建議是這里最好別使用多重繼承)
3. 不要內(nèi)嵌std::string對象或者其他拷貝構(gòu)造可能拋出異常的數(shù)據(jù)成員、基類。在上述點拋出異常將導(dǎo)致直接調(diào)用std::terminate().讓基類或數(shù)據(jù)成員的默認(rèn)構(gòu)造函數(shù)可能拋出異常也是同樣糟糕的主意,你本來是打算通過一個包含對象構(gòu)造的throw表達(dá)式報告異常, 程序卻無謂地中止了:
throw some_exception();
當(dāng)發(fā)生異常拷貝時,有幾種方法避免復(fù)制字符串對象,例如在異常對象中嵌入一個定長存儲區(qū),或者通過引用計數(shù)來管理字符串。不過,在采用這些方法前,先考慮考慮下一條。
4. 只在確實需要的時候才格式化what()返回的信息。格式化是一個典型的內(nèi)存相關(guān)的操作,有可能拋出異常。最好把格式化推遲到棧展開之后,因為棧展開可能釋放某些資源。對what()函數(shù)用catch(...)塊加以保護(hù)是一個好主意,這樣你就可以在格式化拋出異常時有了一個退路。
5. 不要太在意what()的信息。在異常拋出點,對程序員來說,這是給出錯誤信息的好機(jī)會,但是你未必能夠把相關(guān)信息組合成用戶可以理解的形式。國際化就一個典型的情況。Peter Dimov給出了良好建議:建一個錯誤信息格式化的表格,把what()的字符串作為這個表的鍵。當(dāng)標(biāo)準(zhǔn)庫拋出異常時,如果我們只能獲得其標(biāo)準(zhǔn)的what()字符串……
6. 在異常類的public接口中暴露導(dǎo)致錯誤的有關(guān)信息。返回固定信息的what()意味著你忽視了暴露信息,而用戶可能需要提供相關(guān)信息。例如,你的異常想報告數(shù)字范圍錯,報錯的代碼應(yīng)該能夠透過異常的公共接口讓異常包含導(dǎo)致問題的那個變量值。如果你只是在what()中以文本方式表現(xiàn)這些數(shù)字,那些需要根據(jù)信息做更多(或更少)處理的程序員日子將很難過。
7. 如果可能,讓你的異常類對兩次析構(gòu)免疫。幾款流行的編譯器偶爾會使異常對象被銷毀兩次。如果你能采取措施防御危害(比如,把釋放的指針置零)就可以使代碼更健壯。
如何處理程序員犯錯?
作為開發(fā)者,如果我違反了所使用庫的某個前條件,我不希望棧展開。我希望的是core dump或者等價物—一個能精確地在問題發(fā)生點檢查程序狀態(tài)的方法。這通常意味著assert()或者其他類似的東西。
有時候為用戶提供可以應(yīng)付任意誤用的強(qiáng)健的API是有必要的,但這樣通常要付出不菲的代價。比如,一個常見需求是跟蹤客戶使用的每一個對象,從而可以驗證合法性。如果你需要這種保護(hù),通常是在一個簡單API上再封裝一層來實現(xiàn)。盡管你做得小心翼翼,有強(qiáng)健承諾的API也只能防御某些而不是所有會導(dǎo)致災(zāi)難的誤用。客戶也開始依賴那些保護(hù)并且所依賴的保護(hù)也將增長到接口保護(hù)不到的部分。
windows開發(fā)者請注意:當(dāng)你使用assert()時,大部分Windows編譯器實際上都是拋出異常,并且被本地截獲,這很不幸。事實上,截獲的錯誤經(jīng)常是段訪問失敗或者除零錯。當(dāng)你使用JIT(Just In Time)調(diào)試時這是個問題,這意味著在在喚醒調(diào)試器之前已經(jīng)異常棧展開了,因為catch(…)將捕獲這個異常,其實這個并非C++異常。幸運(yùn)的是,有一個鮮為人知的簡單辦法可以處理:
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); |
這個方法無法應(yīng)付在catch塊中(或者catch塊調(diào)用的函數(shù)中)拋出結(jié)構(gòu)化異常的情況,但它確實可以解決絕大多數(shù)JIT導(dǎo)致的問題。
該如何處理異常?
壓根就不處理異常一般是處理異常的最好辦法。如果你讓異常穿越你的代碼,并且在析構(gòu)函數(shù)中做清理工作,代碼會更干凈。
盡可能避免catch(…)
很不幸,其他非Windows操作系統(tǒng)一樣會把非C++異常(例如線程中止)卷入到C++異常機(jī)制中去,而且,有時候也沒有類似上面提到的_set_se_translator這樣的hack手法加以解決。我們通常在析構(gòu)函數(shù)或者catch塊中做合理操作來維持系統(tǒng)的不變式,這通常是安全的。然而catch(...)也會捕獲非預(yù)期的系統(tǒng)通知,這時是不可能像對待普通C++異常一樣來處理的,慣用的手法不再安全了。
經(jīng)過新聞組上長期的辯論之后,盡管不情愿,我還是得承認(rèn)Hillel Y. Sims觀點:除非所有操作系統(tǒng)修正前面的問題,否則,所有異常應(yīng)該繼承自std::exception,當(dāng)所有人適應(yīng)catch(std::exception&)而不是catch(...)時,世界將會更加美好。
即使不考慮和操作系統(tǒng)間糟糕的交互情況,有時候,catch(...)仍然是最合適的選擇。如果你根本不知道會有什么異常拋出,并且必須停止棧展開,這可能是你唯一出路。一個典型的情況就是跨語言的時候。