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