C++異常安全的思考

IceCrystals發表於2013-11-11

異常安全的程式碼是指,滿足兩個條件 

1異常中立性 :
是指當你的程式碼(包括你呼叫的程式碼)引發異常時,這個異常 能保持原樣傳遞到外層呼叫程式碼
2.異常安全性:
 1,丟擲異常後,資源不洩露,
2,丟擲異常後,不會使原有資料惡化(例如正常指標變野指標)
3。。少些try catch,因為大量的try catch會影響程式碼邏輯。導致程式碼醜陋混亂不優雅
一段程式碼要具有異常安全性,必須同時具有異常中立性和一定等級的異常安全性保證
異常安全的等級一般有:
1,函式提供基本保證(the basic guarantee)(不會發生記憶體洩漏並且程式內的每個物件都處在合法的狀態,沒有流錯位,沒有野指標,但是不是每個物件的精確狀態是可以預期的,如可能發生異常後,指標處在空指標狀態,或者某種預設狀態,但是客戶無法預知到底是哪一個),對於達成基本保證可以多使用智慧指標來管理資源
2,函式提供強力保證(the strong guarantee),強力保證含義是,成功或者回滾保證,發生異常的函式對程式來說,沒有任何改動,提供發生異常時候的回滾機制。
呼叫提供強力保證的函式之後,僅有兩種可能的程式狀態:像預期一樣成功執行了函式,或者函式回滾繼續保持函式被呼叫時當時的狀態。與之相比,如果呼叫只提供基本保證的函式引發了異常,程式可能存在於任何合法的狀態。
函式提供強力保證的有效解決辦法是
copy-and-swap:
先做出一個你要改變的物件的copy,然後在這個copy上做出全部所需的改變。如果改變過程中的某些操作丟擲了異常,最初的物件保持不變。在所有的改變完全成功之後,將被改變的物件和最初的物件在一個不會丟擲異常的操作中進行swap。
3. 函式有不丟擲保證(the nothrow guarantee),對於所有對內建型別(例如,ints,指標,等等)的操作都是不丟擲(nothrow)的(也就是說,提供不丟擲保證)。這是異常安全程式碼中必不可少的基礎構件。
注意事項:
異常安全 最關鍵的是:swap ctor dctor 不發生異常保證,只有成功或者終止程式兩種狀態
一個函式的異常安全等級,是取決於它所呼叫的函式中最低異常安全等級的函式。
C++11新增了noexcept關鍵字,在void func() noexcept{}noexcept保證了這個函式不會丟擲異常,只有終止程式和成功執行兩種狀態。
noexcept可以接受一個常量表示式,noexcept(constexpr。。。)當常量表示式為轉換為true說明該函式保證不丟擲異常。
 
從異常安全的觀點看,不丟擲的函式(nothrow functions)是極好的,但是在 C++ 的 C 部分之外部不呼叫可能丟擲異常的函式簡直就是寸步難行。使用動態分配記憶體的任何東西(例如,所有的 STL 容器)如果不能找到足夠的記憶體來滿足一個請求,在典型情況下,它就會丟擲一個 bad_alloc 異常。只要你能做到就提供不丟擲保證,但是對於大多數函式,選擇是在基本的保證和強力的保證之間的。
 
但是,不是所有函式都能做出異常保證的,考慮這樣一個函式,函式內部的函式內是一個對資料庫的操作,一旦異常發生,難以撤銷對資料庫的更改。如果想對這樣的函式做到異常的strong guarantee保證,就是非常困難度事情。
所以對於只對區域性變數改變的函式保證異常安起會相對比較容易。如果函式的操作中牽扯到全域性變數等等,就變得困難的多。
解決異常安全的好辦法:
1,多使用RAII,使用智慧指標來管理記憶體。由於unwind機制的保證,當異常發生時,函式棧內已構造的區域性物件的解構函式會被一一呼叫,在解構函式內釋放資源,也就杜絕了記憶體洩漏的問題。
2,做好程式設計。特別是異常發生時的回滾機制的正確使用,copy-and-swap是有效的方法。
3,注意需要異常保證的函式內部的呼叫函式,異常安全等級是以有最低等級異常保證的函式確定的。
一個系統即使只有一個函式不是異常安全的,那麼系統作為一個整體就不是異常安全的,因為呼叫那個函式可能發生洩漏資源和惡化資料結構。
4,對於一些需要保證效能的程式,在提供基本的異常安全時,要注意,棧解退機制只是呼叫解構函式,對於內建型別的操作不會被回滾,所以。像起累加器作用的一些內建型別變數,應該注意在函式成功執行後再進行累加。避免資料結構惡化。重新分配資源給原本已經持有資源的變數,應該先清空釋放變數的資源,指標再設定為nullptr,防止資源重新分配過程中丟擲異常,導致指標變為野指標的問題。
5,流物件,資源物件,new物件,不應該直接作為引數,一旦丟擲異常,就可能會導致嚴重的問題,函式也許會被錯誤的執行,資源也許會洩漏。對於函式引數和函式內使用的全域性變數,應該保證在進入函式內部是是正常狀態。
6.減少全域性變數的使用,對包含全域性變數的函式做異常安全是比較困難的事情,棧解退也只對區域性變數起效果。
7,如果不知道如何處理異常,就不要捕獲異常,直接終止比吞掉異常不處理要好
8.保證 構造 析構 swap不會失敗
 
這裡有個注意事項:
在建構函式中,如果丟擲異常,是不會呼叫當前正在構造的類的解構函式的,因為當前正在構造的類沒有構造完成,只會析構已經構造完成成員和父類,So,極易導致記憶體洩漏,這裡要謹慎處理,使用RAII,智慧指標,noexcept保證不會丟擲異常和惡化資料。
 
參考文章:
1:物件生死劫-建構函式和解構函式異常
2:C++箴言:爭取異常安全的程式碼
3:如何編寫異常安全的程式碼

相關文章