c++ - std:: exception




從析構函數中拋出異常 (11)

大多數人說永遠不會從析構函數中拋出異常 - 這樣做會導致未定義的行為。 Stroustrup指出: “向量析構函數明確地為每個元素調用析構函數,這意味著如果一個元素析構函數拋出,向量銷毀失敗......真的沒有什麼好辦法來防止從析構函數中拋出異常,所以庫如果元素析構函數拋出不作任何保證“(來自附錄E3.2)

這篇文章似乎另有說法 - 拋出析構函數或多或少都可以。

所以我的問題是 - 如果從析構函數中拋出導致未定義的行為,那麼如何處理析構函數期間發生的錯誤?

如果在清理操作過程中發生錯誤,您是否忽略它? 如果它是一個可能在堆棧中處理的錯誤,但在析構函數中不正確,那麼將異常拋出析構函數沒有意義嗎?

顯然這些錯誤是罕見的,但可能的。


問:所以我的問題是 - 如果從析構函數中拋出導致未定義的行為,那麼如何處理析構函數期間發生的錯誤?

答:有幾種選擇:

  1. 讓異常從析構函數中流出,而不管其他地方發生了什麼。 在這樣做時要注意(或者甚至害怕)std :: terminate可能會隨之而來。

  2. 永遠不要讓異常流出你的析構函數。 可以寫入日誌,如果可以的話,可以寫一些大的紅色壞文本。

  3. 我的愛人 :如果std::uncaught_exception返回false,讓你的例外流出。 如果它返回true,則返回到日誌記錄方法。

但投擲骰子好嗎?

我同意上面的大部分內容,最好避免在析構函數中拋出throw,它可以是。 但有時你最好接受它可能發生的事情,並處理好。 我會選擇3以上。

有一些奇怪的情況是從析構函數中拋出它的一個好主意 。 像“必須檢查”錯誤代碼一樣。 這是從函數返回的值類型。 如果調用者讀取/檢查包含的錯誤代碼,則返回的值將無聲地破壞。 但是 ,如果在返回值超出範圍時返回的錯誤代碼還沒有被讀取,它將從析構函數中拋出一些異常。


所以我的問題是 - 如果從析構函數中拋出導致未定義的行為,那麼如何處理析構函數期間發生的錯誤?

主要問題是:你不能失敗 。 畢竟,失敗意味著什麼? 如果向數據庫提交事務失敗,並且失敗(無法回滾),那麼數據的完整性會發生什麼變化?

由於析構函數是針對正常和異常(失敗)路徑調用的,因此它們本身不會失敗,否則我們“無法失敗”。

這是一個概念上難以解決的問題,但通常解決方法是找到一種方法來確保失敗不會失敗。 例如,數據庫可能會在提交到外部數據結構或文件之前寫入更改。 如果事務失敗,那麼可以拋棄文件/數據結構。 所有它必須確保的是,提交來自該外部結構/文件的更改是不可能失敗的原子事務。

務實的解決方案或許只是確保失敗失敗的可能性是天文數字不可能的,因為在某些情況下使事情不可能失敗是幾乎不可能的。

對我來說最合適的解決方案是編寫非清理邏輯,使清理邏輯不會失敗。 例如,如果您想要創建一個新的數據結構來清理現有的數據結構,那麼您可能會試圖提前創建該輔助結構,以便我們不必再在析構函數內創建它。

誠然,說起來容易做起來容易,但這是我看到的唯一真正適合的方式。 有時我認為應該有能力為正常執行路徑編寫單獨的析構函數邏輯,以避免出現異常情況,因為有時析構函數會感覺有點像他們通過嘗試處理這兩個函數來承擔雙重責任(例如需要明確解僱的範圍守護程序;如果他們能夠區分特殊的銷毀途徑和非特殊的銷毀路徑,他們就不會要求這樣)。

仍然最終的問題是我們不能失敗,而且在所有情況下都是完美解決的概念設計難題。 如果你不太複雜的控制結構中包含大量的小物件,它們會變得更容易,而這些結構中的很多小物件會相互作用,而是以稍大一點的方式模擬您的設計(例如:帶有析構器的粒子系統來摧毀整個粒子系統,而不是每個粒子的單獨的非平凡析構函數)。 當你在這種較粗糙的級別上設計你的設計時,你需要處理更少的不平凡的析構函數,並且通常也可以承擔任何需要的內存/處理開銷,以確保析構函數不會失敗。

這是最簡單的解決方案之一,自然是不經常使用析構函數。 在上面的粒子示例中,也許在摧毀/去除粒子時,應該完成一些可能因任何原因而失敗的事情。 在這種情況下,不是通過粒子的dtor調用這種可以在特殊路徑上執行的邏輯,而是可以在粒子系統移除粒子時由粒子系統完成。 去除粒子可能總是在非常規路徑中完成。 如果系統被破壞,也許它可以清除所有的粒子,而不用去掉可能失效的單個粒子去除邏輯,而只有在粒子系統正常執行時才會執行可能失敗的邏輯,當它移除一個或多個粒子時。

如果避免使用不平凡的析構函數處理大量小型對象,那麼通常會出現類似的解決方案。 在那裡你可能會陷入一團糟,看起來幾乎不可能是異常 - 安全是當你糾纏在許多小巧的物體上時,它們都有非平凡的時間。

如果nothrow / noexcept實際上被翻譯成編譯器錯誤,如果任何指定它的東西(包括應該繼承其基類的noexcept規範的虛擬函數)試圖調用任何可能拋出的東西,它會有很大幫助。 這樣我們就可以在編譯時捕獲所有這些東西,如果我們實際上無意中編寫了一個可能拋出的析構函數。


作為對主要答案的補充,這些答案是好的,全面的和準確的,我想對你引用的文章發表評論 - 說“在析構函數中拋出異常並不是那麼糟糕”。

文章採用了“拋出異常的替代方法是什麼”這一行,並列出了每個替代方案的一些問題。 這樣做後得出結論,因為我們找不到無問題的選擇,所以我們應該繼續拋出異常。

麻煩的是,它所列出的替代品的問題都不像異常行為那麼糟糕,我們記得它是“未定義的程序行為”。 作者的一些反對意見包括“審美醜陋”和“鼓勵不良風格”。 現在你想要哪一個? 一個風格不好的程序,或者一個表現出不確定行為的程序?


你的析構函數可能在其他析構函數的鏈中執行。 拋出未被直接調用者捕獲的異常可能會使多個對象處於不一致狀態,從而導致更多問題,從而忽略清理操作中的錯誤。


問自己關於從析構函數拋出的真正問題是“調用者可以用這個來做什麼?” 有沒有什麼有用的,你可以做的例外,這將抵消從析構函數拋出創建的危險?

如果我摧毀了一個Foo對象,並且Foo析構函數拋出了一個異常,我可以合理地使用它嗎? 我可以記錄它,或者我可以忽略它。 就這樣。 我不能“修復”它,因為Foo像已經消失了。 最好的情況下,我記錄異常並繼續,如果沒有任何事情發生(或終止程序)。 通過從析構函數中拋出是否真的有可能導致未定義的行為?


它是危險的,但從可讀性/代碼可理解性的角度來看,它也沒有意義。

你必須要問的是在這種情況下

int foo()
{
   Object o;
   // As foo exits, o's destructor is called
}

什麼應該抓住例外? 應該是foo的調用者嗎? 或者應該處理它? foo的調用者為什麼應該關心foo內部的某個對象? 這種語言可能有一種定義這是有道理的,但它會變得難以理解和難以理解。

更重要的是,Object的內存何去何從? 對象擁有的內存在哪裡去? 它仍然分配(表面上是因為析構函數失敗)? 考慮到對像是在堆棧空間 ,所以它顯然不管。

然後考慮這種情況

class Object
{ 
   Object2 obj2;
   Object3* obj3;
   virtual ~Object()
   {
       // What should happen when this fails? How would I actually destroy this?
       delete obj3;

       // obj 2 fails to destruct when it goes out of scope, now what!?!?
       // should the exception propogate? 
   } 
};

當刪除obj3失敗時,我如何以確保不會失敗的方式實際刪除? 它是我的記憶!

現在考慮在第一個代碼片段中,Object自動消失,因為它在堆棧上,而Object3在堆上。 由於指向Object3的指針消失了,你就是SOL。 你有內存洩漏。

現在做一件安全的事情是以下幾點

class Socket
{
    virtual ~Socket()
    {
      try 
      {
           Close();
      }
      catch (...) 
      {
          // Why did close fail? make sure it *really* does close here
      }
    } 

};

另請參閱此FAQ


從析構函數中拋出異常是很危險的。
如果另一個異常已經傳播,應用程序將終止。

#include <iostream>

class Bad
{
    public:
        // Added the noexcept(false) so the code keeps its original meaning.
        // Post C++11 destructors are by default `noexcept(true)` and
        // this will (by default) call terminate if an exception is
        // escapes the destructor.
        //
        // But this example is designed to show that terminate is called
        // if two exceptions are propagating at the same time.
        ~Bad() noexcept(false)
        {
            throw 1;
        }
};
class Bad2
{
    public:
        ~Bad2()
        {
            throw 1;
        }
};


int main(int argc, char* argv[])
{
    try
    {
        Bad   bad;
    }
    catch(...)
    {
        std::cout << "Print This\n";
    }

    try
    {
        if (argc > 3)
        {
            Bad   bad; // This destructor will throw an exception that escapes (see above)
            throw 2;   // But having two exceptions propagating at the
                       // same time causes terminate to be called.
        }
        else
        {
            Bad2  bad; // The exception in this destructor will
                       // cause terminate to be called.
        }
    }
    catch(...)
    {
        std::cout << "Never print this\n";
    }

}

這基本上歸結為:

任何危險的(即可能拋出異常)都應該通過公共方法(不一定是直接)完成。 然後,您的類的用戶可以通過使用公共方法並捕獲任何潛在的異常來處理這些情況。

然後析構函數將通過調用這些方法來完成對象(如果用戶沒有明確這樣做),但是任何異常拋出都會被捕獲並丟棄(在嘗試修復問題之後)。

所以實際上你將責任傳遞給用戶。 如果用戶能夠糾正異常,他們將手動調用相應的功能並處理任何錯誤。 如果對象的用戶不擔心(因為對象將被銷毀),那麼析構函數被留下來處理業務。

一個例子:

的std :: fstream的

close()方法可能會引發異常。 如果文件已被打開,析構函數調用close(),但確保任何異常不會傳播出析構函數。

因此,如果文件對象的用​​戶想要對與關閉文件相關的問題進行特殊處理,他們將手動調用close()並處理任何異常。 另一方面,如果他們不在乎,那麼解析器將被留下來處理這種情況。

Scott Myers在他的書“Effective C ++”中有一篇關於該主題的優秀文章,

編輯:

顯然,在“更有效的C ++”
項目11:防止異常離開析構函數


我們必須在此區分 ,而不是盲目地遵循針對具體情況的一般建議。

請注意,以下內容忽略了對象容器的問題以及面對容器內多個對象時要做什麼。 (部分對象可能會被忽略,因為有些對像不適合放入容器。)

當我們以兩種類型拆分類時,整個問題變得更容易思考。 一個類可以有兩個不同的職責:

  • (R)釋放語義(aka釋放內存)
  • (C) 提交語義(即flush file到磁盤)

如果我們用這種方式來看待這個問題,那麼我認為可以認為(R)語義不應該導致一個例外,因為存在這樣一個例外:我們無法做到這一點,並且b)許多自由資源操作不甚至提供錯誤檢查,例如void free(void* p);

具有(C)語義的對象,例如需要成功刷新其數據的文件對像或在Dtor中執行提交的(“範圍防護”)數據庫連接,具有不同的類型:我們可以對錯誤進行操作應用程序級別),我們真的不應該繼續,因為沒有任何事情發生。

如果我們遵循RAII路線並且允許在它們的角色中具有(C)語義的對象,那麼我認為我們還必須考慮到這種情況可能出現的奇怪情況。 由此可見,不應該將這些對象放入容器中,而且如果在另一個異常處於活動狀態時拋出commit-dtor,程序仍然可以terminate()

關於錯誤處理(提交/回滾語義)和異常, Andrei Alexandrescu有一個很好的演講: C ++ /聲明控制流程中的錯誤處理 (在NDC 2014舉辦)

在細節中,他解釋了Folly庫如何為ScopeGuard工具實現UncaughtExceptionCounter

(我應該注意到others也有類似的想法。)

雖然談話並不關注擲骰子,但它展示了一種工具,可以用來擺脫拋出時間的問題

未來 ,這可能有一個標準功能, 請參閱N3614並對此進行討論

更新'17:C ++ 17標準功能是std::uncaught_exceptions afaikt。 我會盡快引用cppref文章:

筆記

使用int -returning uncaught_exceptions的示例是......首先創建一個警戒對象,並在其構造函數中記錄未捕獲異常的數量。 輸出由guard對象的析構函數執行,除非foo()拋出( 在這種情況下,析構函數中的未捕獲異常的數量大於構造函數觀察到的數量


我目前遵循這樣的政策(很多人都這樣說),即班級不應該主動從他們的析構者中拋出異常,而應該提供一種公開的“關閉”方法來執行可能失敗的操作。

...但我確實認為容器類類的析構函數(如向量)不應該掩蓋它們包含的類拋出的異常。 在這種情況下,我實際上使用遞歸調用自己的“自由/關閉”方法。 是的,我遞歸地說。 有這種瘋狂的方法。 異常傳播依賴於存在堆棧:如果發生單個異常,那麼剩餘的析構函數仍將運行,並且一旦例程返回,掛起的異常將傳播,這非常好。 如果發生多個異常,那麼(取決於編譯器)第一個異常將傳播或程序將終止,這是可以的。 如果發生如此多的異常以致遞歸溢出堆棧,那麼有些事情是嚴重錯誤的,並且有人會發現它,這也是可以的。 就我個人而言,我犯的錯誤是錯誤的,而不是隱藏的,秘密的和陰險的。

關鍵是容器保持中立,並且由包含的類決定他們是否在從析構函數中拋出異常方面行為或行為不當。


拋出析構函數可能會導致崩潰,因為此析構函數可能被稱為“堆棧展開”的一部分。 堆棧展開是拋出異常時發生的過程。 在這個過程中,所有從“try”開始直到拋出異常被推入堆棧的對象將被終止 - >它們的析構函數將被調用。 在這個過程中,另一個異常拋出是不允許的,因為一次不能處理兩個異常,因此這會引發一個調用abort(),程序將崩潰,控制權將返回到操作系統。


設置鬧鐘事件。 通常,警報事件是清理物體時通知故障的更好形式





raii