條款29:爲"異常安全"而努力是值得的

條款29:爲"異常安全"而努力是值得的
    (Strive for exception-safe code.)
   
內容:
    看了這款的內容,我對C++的難以控制的"脾氣"又有了進一步的瞭解,C++的安全性問題一直是廣大非C++程序
員所抨擊C++語言"惡行"的主要方面,我們今天討論的其"異常安全性"也是其複雜之處之一,看完這一款之後,你也許
會發覺你以前寫的代碼可能會給最終產品帶來多大的"風險隱患",廢話我就不多說了,開始進入今天的話題.
    按照老規矩來,先看一個例子:假設有個class用來表現夾帶背景圖案的GUI菜單,這個class也要用於多線程
環境當中,所以我們考慮用了一個互斥器(mutex)作爲併發控制(concurrency control)之用:
    class PrettyMenu{
    public:
        ...
        void changeBackground(std::istream& imgSrc); //改變圖片背景
        ...
    private:
        Mutex mutex_;       //互斥器
        Image* bgImage_;    //目前的背景圖像
        int imageChangesCounts_; //背景圖像被改變的次數
    };
    下面是一個可能的實現:
    void PrettyMenu::changeBackground(std::istream& imgSrc){
        lock(&mutex_);          //取得互斥器
        delete bgImage_;        //擺脫舊的背景圖像
        ++imageChangesCounts_;  //修改變更次數
        bgImage_ = new Image(imgSrc); //安裝新的背景圖像
        unlock(&mutex_);        //釋放互斥器
    }
    從"異常安全性"的角度來看,這個真是個糟糕的實現版本,至少我們能夠指出如下兩點不足:(1)如果Image構造
函數拋出異常,那麼mutex_就永遠不能得到unlock;(2)此外如果構造沒有成功,而imageChangesCounts_是在構造
之前執行,那麼其自然就出現了"數據失真"的風險.
    解決問題(1)我們可以用對象管理資源方法進行解決(條款13中提到),我們只要把imageChangesCounts_的自
增操作放在對象產生之後也就可以解決問題(2)了,該實現先修改如下:
    void PrettyMenu::changeBackground(std::istream& imgSrc){
        Lock(&mutex_);          //獲得互斥器並確保它稍後被釋放
        delete bgImage_;       
        bgImage_ = new Image(imgSrc);
        ++imageChangesCounts_; 
    }
    進一步我們可以把刪除操作放在智能指針內部完成:
    class PrettyMenu{
        ...
        std::tr1::shared_ptr<Image> bgImage_;
    };
    void PrettyMenu::changeBackground(std::istream& imgSrc){
        Lock(&mutex_);          //獲得互斥器並確保它稍後被釋放   
        bgImage_.reset(new Image(imgSrc));
        ++imageChangesCounts_; 
    }
    代碼是不是簡潔多了,呵呵.看起來這樣使得"異常安全機制"很完美嘛,不過"美中不足"的是:如果Image構造函數出
現異常,那麼有可能輸入流imageSrc的讀取記號將被移走.這樣的話,該函數也只能提供"基本的異常安全保證".什麼是
"基本的異常安全保證",別急,挺我給你慢慢道來,作爲異常安全函數必須提供以下三個保證之一:
    ■ 基本承諾:如果異常被拋出,程序內的任何事物仍然保持在有效狀態下.
    ■ 強烈保證:如果異常被拋出,程序狀態不改變.
    ■ 不拋擲(nothrow)保證:承諾絕不拋出異常,因爲它們總是能夠完成它們原先承諾的功能.
    理解了上面三種專業屬於後,我們來把目光再次聚焦到上面changeBackground的代碼實現上,看看現在的代碼
,已經很好了呀,由於先前的那個小不足,使得我的這段代碼還是沒有達到"異常安全的強烈保證",我不放棄,四處尋找解
決方案終於有個一般化的設計策略可以達到這個目的,這個策略被稱爲"copy and swap",俗話說"名如其人",這個原則其
實就是:爲你打算修改的對象保存一個副本,然後在該副本上修改.若修改過程中發生異常,問題不大,呵呵,因爲原
對象狀態沒有被改變嘛.修改動作完成以後進行"副本與原對象互換"操作.真是個不錯的方案,我心理不由的贊一個,下
面我們就開始改代碼:
    struct PMImpl{
        std::tr1::shared_ptr<Image> bgImage_;
        int imageChangesCounts_;
    };
    class PrettyMenu{
        ...
        void changeBackground(std::istream& imgSrc){
            using std::swap;               //噠噠噠噠,條款25我們談到這個swap的實現方法的喔!
            Lock m1(&mutex_);
            std::tr1::shared_ptr<PMImpl> pNewImpl(new PMImpl(*pImpl_)); //make copy
            pNewImpl->bgImage_.reset(new Image(imgSrc));//handle copy
            ++pNew->imageChangesCounts_;
            swap(pImpl_,pNewImpl); //swap
        }
    private:
        Mutex mutex_;
        std::tr1::shared_ptr<PMImpl> pImpl_;
    };
    NND,代碼量又增多了,有得必有失嘛,想開點!我們注意到copy-and-swap的關鍵在於"修改對象數據副本,然後在
一個不拋異常的函數中將修改後的數據和原件置換",因此必須爲每一個即將被改動的對象做一個副本,那得耗用你
可能無法(無意願)供應的時間和空間.這是一個很實際的問題:我們都希望提供"強烈保證";當它可被實現時你的確應
該提供它,但"強烈保證"並非在任何時刻都顯得實際.當強烈保證不切實際的時候,你就必須提供基本保證.在實際的開
發當中你可以爲某些函數提供強烈保證,但效率和複雜度帶來的成本會使得你不得不去放棄它,萬一實際不可行,使
你退而求其次地只提供基本保證,任何人都不該因此責難你.對許多函數而言,"異常安全性之基本保證"是一個絕對通情達
理的選擇.
    累死我了,到此結束.
    請記住:
    ■ 異常安全函數即使發生異常也不會泄漏資源或允許任何數據結構被破壞.這樣的函數區分爲三種可能的保證:
基本型、強烈型、不拋異常型.
    ■ "強烈保證"往往能夠以copy-and-swap實現出來,但"強烈保證"並非對所有函數都可實現或具備現實意義.
    ■ 函數提供的"異常安全保證"通常最高只等於其所調用之各個函數的"異常安全保證"中的最弱者.
   

發佈了62 篇原創文章 · 獲贊 4 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章