1. 程式人生 > >C++中的異常安全性【轉】

C++中的異常安全性【轉】

div 現實 操作 ... 基本 數據 安全性 異常安全 做的

原文寫的非常好,來自這裏

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

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

void Type::Func()  
{  
    Lock(&mutex);  
    DoSomething();  
    UnLock(&mutex);  
}  

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

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

class Type
{
public:
    ....
    Type& operator = (const Type &t)
    {
        
if(this == &t) return *this; else { delete m_t; m_t = new T(t->m_t); return *this; } } .... private: T *m_t; };

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

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

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

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

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

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

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

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

void Type::Func()  
{  
    MutexLock ml(&mutex);  
    DoSomething();  
}  

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

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

Type& Type::operator = (const Type &t)  
{  
    Type tmp(t);  
    swap(m_t,tmp->m_t);  
      
    return *this;  
}  

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

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

void Func()  
{  
    f1();  
    f2();  
}  

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

總結:

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

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

C++中的異常安全性【轉】