為什麼C++中,析構函數、operator delete、以及operator delete []按照慣例不會拋出異常?
最近在讀C++ Common Knowledge Essential Intermediate Programming,看到Item 38,異常安全公理,其中的公理2中提到按照慣例,析構函數、operator delete、以及operator delete []不會拋出異常。並且說如果允許異常從析構函數傳播出去的話,將會收到批評、排斥、為社會所不容。
但是我實在不是很了解這其中的原因,本人學習C++的小學生,希望各位大大能夠賜教。
不是不會,是不能。這是你自己要確保的。如果你寫出了一個會拋異常的析構函數和operator delete,那就是你的bug要改。
至於原因嘛。一般來說new掛了,try-catch會幫你析構掉那些new之前成功處理的東西。當然了你的構造函數自己要try-catch恢復,因為構造函數失敗了,類是不完整的,所以相對應的那個析構函數是不運行的。如果這個時候delete也掛了,那簡直無法恢復了。以前我看Windows關於如何在x86處理器上實現try-catch的文章曾經指出(我只剩下一點印象了不保證完全正確),嵌套拋出的異常在某些情況下有可能會忽略所有stack unwind。也就是說你正常的、在上層的函數的try-catch裡面要跑的析構函數和所有的catch,不運行了。
因為 C++ 在語言層面上保證「『異常從被拋出到被捕獲之間的清理工作』如因為拋出異常而退出,則自動調用 std::terminate 退出整個程序」。
那麼「從被拋出到被捕獲之間的清理工作」是什麼呢?棧展開(stack unwinding):調用各種析構函數,析構函數里說不定還會調用 operator delete 和 operator delete[]。
所以如果你從析構函數、operator delete 或者 operator delete[] 里拋異常出來而不處理,用了你寫的代碼的程序就有必定會因此而立刻退出,而且完全無法避免。你能不「收到批評、排斥、為社會所不容」嗎?好問題。
幾位大牛說得也都是對的,1.不是不會,是不能,不被允許。2.確實是因為stack unwind的關係。可是好奇的題主顯然不會滿足與此啊,說了那麼多,到底"stack unwind"是個啥啊?
對於這個問題的終極回答,就是我這裡提到的兩篇奇文。
如何用C語言實現異常/狀況處理機制? - 邰原朗的回答如果嫌文章太長,我就先描個大概。
你的C++程序拋了一個異常,
從throw拋出到相應的catch段落被執行,之間發生了什麼事?假設是這樣的call stack:
top_level(); &<== 這裡有一個try_catch
└── layer_1(); └── layer_2();└── layer_3(); &<== 邦!這裡throw
throw其實觸發了軟中斷,
軟中斷的handler是這樣的:{ layer_3() 裡面有沒有適當的catch? 沒有。 layer_2() 裡面有沒有適當的catch? 沒有。 layer_1() 裡面有沒有適當的catch? 沒有。 top_layer() 裡面有沒有適當的catch? (緊握雙手熱淚盈眶)同志!終於找到你了。同志,請做catch處理,我要long jump到你這裡來了喲!
等等!layer3()中有沒有局部變數需要析構一下? 有的,我先去析構他們。
好了! 那layer2()中有沒有局部變數需要析構一下? 有的,我去析構他們。 WTF! layer2()中又一個變態,在析構函數里又拋一個異常?怎麼辦,上一個異常還沒有處理好,又來一個。。。
我靠,我真不會了,乾脆退出進程吧! }你看,一起dtor異常引發的血案就是這樣的。可以拋,但出錯的話,你的程序會 直接terminate. 這可能不是你想要的。至少得寫個 log 之類的吧。
析構函數被調用的一個情況是在已經發生exception正在 unwind stack時。unwind stack會調用stack中對象的析構函數。這個時候如果再出現異常,如果你是 C++,你說你怎麼辦?你只能中斷 unwind stack 直接退出。程序中的其他邏輯,比如寫log,將不會被執行。好像沒有更好更合適的解決辦法。補充一點,以下可以偵測到stack unwind:
bool uncaught_exception() noexcept;
Returns true if an exception has been thrown but the initialization of the exception declaration in the matching handler (including an automatic call to unexpected or terminate) is not yet complete.
Returns false in all other cases, including when unexpected or terminate is explicitly called by the program.
Throwing another exception while this function returns true may result in the termination of the exception handling proccess (i.e., an automatic call to terminate).
以下設置terminate handler:terminate_handler set_terminate (terminate_handler f) noexcept;
假設你在殺豬,一刀子下去發現自己殺錯了,拋了個異常不殺了。你說這個豬現在是活豬還是死豬。
薛定諤的豬。。。這個問題我也仔細研究過,但並沒有找到很權威的答案。effective c艹也提過,不應該在析構拋出異常,我總結的原因是發生異常時,會自動調用局部變數的析構函數,如果此時析構拋出異常,就會有兩個未處理的異常存在,標準規定此時程序應當終止(存疑,沒去核查標準),我想是為了編譯器好處理,否則又要制定規則兩個異常同時存在時的先後處理,catch規則等…
這裡用一個故事作為比喻來說明為什麼不能讓異常逃離析構函數:
假設你正在玩一款遊戲:
- throw拋出異常:你玩的遊戲出現了某個故障,無法繼續除非讀檔修復
- 異常處理句柄SEH:《遊戲攻略》這本書的目錄,當遊戲出現問題時,會翻開此目錄,沿著目錄檢索解決的辦法
- 你寫的catch:《遊戲攻略》對應章節的解決辦法,每個解決辦法都要以前對應關卡才能完成,於是要讀檔回退到對應關卡。
- windup(棧展開)棧展開:讀檔,時光機,回到過去,回到之前的關卡
程序如何做到較完美地回到過去?
對於棧,比較簡單,棧指針回退到之前的位置即可,PC指針回到對應位置
然而主要注意的是:
(1) 對於比方說某些公共的資源,比如文件,sockcet,動態分配的內存,也需要歸還。
打個比方,我在遊戲中買了車,我讀檔坐著時光機回到過去的時候,發現這輛車已經被「某個人」徵用了,我無法使用,這就不合理。
(2) 為了有序地,正確的歸還資源,最好是按照分配的逆序方式,並且誰分配的,誰歸還(例如C++的RAII)。
為何不能讓異常逃離析構函數
假設你在遊戲中買了輛車,某天換了個新的引擎,結果車開到一半出了故障(throw error),於是,你查看《遊戲攻略》目錄,上面寫到需要時光倒流到買引擎之前的狀態,於是你給引擎的供貨商打電話(析構函數中退回引擎資源)。
然而電話佔線了(析構過程中又出現異常),正常情況比如等待幾分鐘再打電話,然而,你查看《遊戲攻略》,上面寫道:出現給供貨商打電話不通的情況,首先回退到很久遠的版本(因為處理此異常的catch語句在很前面),於是時光倒流之後,發現當初僅僅因為電話沒打通,車也沒了,什麼都沒了,這顯然是不合理。
a. 因為資源必須被析構
b. 所以析構必須得成功
c. 因此析構必須無異常想像一下,假如析構拋出異常,那麼此實例是活是死?如何繼續完整析構?不知道c++primer5 727頁的內容對你有沒有幫助。
推薦閱讀:
※c++ 程序運行時異常處理,怎麼定位到出錯代碼行?
※如何優雅的處理(或忽略)c++函數返回值代表的錯誤?
※python程序報錯後除了try except之外有沒有好的辦法再次啟動?