天天看點

C++中的異常安全性

  一個函數如果說是“異常安全”的,必須同時滿足以下兩個條件:1.不洩漏任何資源;2.不允許破壞資料。 我們先通過兩個反面的例子開始。

       第一個是造成資源洩漏的例子。一個類Type,内含一個互斥鎖成員 Mutex mutex,以及一個成員函數void Func()。假設Func函數的實作如下所示:

  1. void Type::Func()  
  2. {  
  3.     Lock(&mutex);  
  4.     DoSomething();  
  5.     UnLock(&mutex);  
  6. }  

首先是獲得互斥鎖,中間是做該做的事,最後釋放互斥鎖。從功能上來講很完整,沒任何問題。但從異常安全角度來說,它卻不滿足條件。因為一旦DoSomething()函數内部導緻異常,UnLock(&mutex)将不會被執行,于是互斥器将永遠不會被釋放了。換句話說即造成了資源洩漏。

       再來看第二個造成資料破壞的例子。這個例子是我們很熟悉的重載 ‘=’ 操作符的成員函數。依然假設一個類Type,其中一個成員是一個指向一塊資源(假設類型為T)的指針。這時候我們一般就需要來自定義複制構造函數和重載複制操作符以及析構函數了。(絕大多數情況下,這三個成員總是要麼同時存在,要麼都不用定義,因為編譯器預設定義了,即C++中所謂的 ”Rule of 3" 規則。這裡不作詳細介紹)。這裡我們隻考慮重載複制操作符的問題,其部分代碼假設如下:

  1. class Type  
  2. public:  
  3.     ....  
  4.     Type& operator = (const Type &t)  
  5.     {  
  6.         if(this == &t)  
  7.             return *this;  
  8.         else  
  9.         {  
  10.             delete m_t;  
  11.             m_t = new T(t->m_t);  
  12.         }  
  13.     }  
  14. private:  
  15.     T *m_t;  
  16. };  

首先來判斷是否是自我複制,如果是,則直接傳回自己。如果不是,則安全釋放目前指向的資源,再建立一塊與被複制的對象資源一模一樣的資源并指向它,最後傳回複制好的對象。同樣,抛開異常安全來看,沒問題。但是考慮到異常安全性時,一旦“new T(t->m_t)"時抛出異常,m_t将指向一塊已被删除的資源,并沒有真正指向一塊與被複制的對象一樣的資源。也就是說,原對象的資料遭到破壞。

       C++中’異常安全函數”提供了三種安全等級:

       1. 基本承諾:如果異常被抛出,對象内的任何成員仍然能保持有效狀态,沒有資料的破壞及資源洩漏。但對象的現實狀态是不可估計的,即不一定是調用前的狀态,但至少保證符合對象正常的要求。

       2. 強烈保證:如果異常被抛出,對象的狀态保持不變。即如果調用成功,則完全成功;如果調用失敗,則對象依然是調用前的狀态。

       3. 不抛異常保證:函數承諾不會抛出任何異常。一般内置類型的所有操作都有不抛異常的保證。

       如果一個函數不能提供上述保證之一,則不具備異常安全性。

       現在我們來一個個解決上面兩個問題。

       對于資源洩漏問題,解決方法很容易,即用對象來管理資源。RAII技術之前介紹過,這裡就不再贅述。我們在函數中不直接對互斥鎖mutex進行操作,而是用到一個管理互斥鎖的對象MutexLock ml。函數的新實作如下:

  1.     MutexLock ml(&mutex);  

對象ml初始化後,自動對mutex上鎖,然後做該做的事。最後我們不用負責釋放互斥鎖,因為ml的析構函數自動為我們釋放了。這樣,即時DoSomething()中抛出異常,ml也總是要析構的,就不用擔心互斥鎖不被正常釋放的問題了。

       對于第二個問題,一個經典的政策叫“copy and swap"。原則很簡單:即先對原對象做出一個副本(copy),在副本上做必要的修改。如果出現任何異常,原對象依然能保證不變。如果修改成功,則通過不抛出任何異常的swap函數将副本和原對象進行交換(swap)。函數的新實作如下:

  1. Type& Type::operator = (const Type &t)  
  2.     Type tmp(t);  
  3.     swap(m_t,tmp->m_t);  
  4.     return *this;  

先建立一個被複制對象t的副本tmp,此時原對象尚未有任何修改,這樣即使申請資源時有異常抛出,也不會影響到原對象。如果建立成功,則通過swap函數對臨時對象的資源和原對象資源進行交換,标準庫的swap函數承諾不抛出異常的,這樣原對象将成功變成對象 t 的複制版本。對于這個函數,我們可以認為它是”強烈保證“異常安全的。

      當然,提供強烈保證并不是總是能夠實作的。一個函數能夠提供的異常安全性等級,也取決于它的實作。考慮以下例子:

  1. void Func()  
  2.     f1();  
  3.     f2();  

如果f1和f2都提供了”強烈保證“,則顯然Func函數是具有”強烈保證“的安全等級。但是如果f1或f2中有一個不能提供,則Func函數将不再具備”強烈保證“等級,而是取決于f1和f2中安全等級最低的那個。

       總結:

       為了讓代碼具有更好的異常安全性,首先是”用對象來管理資源“,以避免資源的洩漏。其次,在異常安全性等級上,應該盡可能地往更高的等級上來限制。通過 copy-and-swap 方法往往可以實作”強烈保證“。但是我們也應該知道,”強烈保證“并不是對所有的情況都可實作,這取決于你在實作中用到的函數。函數提供的異常安全性的最高等級隻能是你實作中調用的各個函數中異常安全性等級最低的那個。

       參考:《Effective C++》,第三版。

繼續閱讀