異常安全,RAII與C++11


C++11新標准支持lambda表達式后,異常安全的RAII解法就可以簡潔高效了,見下面鏈接文章中的介紹(太好了,這樣以后在我自己的代碼中也能輕松支持上異常安全啦~)

 

http://blog.csdn.net/pongba/article/details/7911997

 

而在之前的C++98中,解法是大牛絞盡腦汁的workaround,介紹如下:

來自CUJ的文章哦,原文:http://www.drdobbs.com/cpp/184403758

本文的 ScopeGuard是 Loki庫的類

 

盡管有點自賣自誇,我還是要在一開始就告訴你,這篇文章里有精彩內容。因為我說服我的好朋友Petru Marginean和我合作寫這篇文章。Petru開發了一個對處理異常很有用的庫。我們一起改進其實現,由此我們得到一個精煉的庫,在特定的情況下,它可以大大簡化異常安全代碼的編寫。
   
    在有異常的情況下要寫出正確的代碼不是一件容易的事,讓我們一起來面對它。異常建立了一個單獨的控制流,它和應用程序的主控制流幾乎沒有關系。要了解異常的控制流需要一種不同的思維方式,並且需要新的工具。
   
    * 寫異常安全的代碼是困難的:一個例子
   
    比如說你正在開發一個現在時髦的即時消息服務器程序。用戶可以登錄和注銷,並且可以互相發送消息。你有一個服務器端的數據庫保存用戶信息,並且在內存里記錄已登錄的用戶。每個用戶可以有好友列表,這個列表同時在內存里和數據庫里保存。
   
    當一個用戶增加或者刪除一個好友時,你需要做兩件事:更新數據庫,並且更新內存中那個用戶的緩存。就這么簡單的一件事。
   
    假設你的模型里每個用戶的信息用一個叫 User 的類來表示,用戶數據庫用 UserDatabase 類表示。增加一個好友的操作看起來就象下面這樣:
   
    class User
    {
        // ...
        string GetName();
        void AddFriend(User& newFriend);
   
    private:
        typedef vector<User*> UserCont;
        UserCont friends_;
        UserDatabase* pDB_;
    };        
   
    void User::AddFriend(User& newFriend)
    {
        // Add the new friend to the database
        pDB_->AddFriend(GetName(), newFriend.GetName());

        // Add the new friend to the vector of friends
        friends_.push_back(&newFriend);
    }   
   
    令人吃驚的是,只有兩行的User::AddFriend里隱藏了一個致命的錯誤。在內存用盡的情況下,vector::push_back會通過拋出異常來表示操作失敗。在那種情況下,你最終只把好友加到數據庫里去,但是沒有加到內存信息里。
   
    現在我們遇到了問題,是嗎?在任何情況下,缺少信息一致性是危險的。很可能在你的應用程序里的很多地方都假設數據庫里的信息和內存里的是同步的。
   
    一個簡單的解決方法是考慮交換兩行代碼的順序:
   
    void User::AddFriend(User& newFriend)
    {
        // Add the new friend to the vector of friends
        // If this throws, the friend is not added to
        // the vector, nor the database
        friends_.push_back(&newFriend);
   
        // Add the new friend to the database
        pDB_->AddFriend(GetName(), newFriend.GetName());
    }   
   
    這確實能在vector::push_back失敗的情況下保護數據一致性。不幸的是,當你查看UserDatabase::AddFriend的文檔,你發現它也會拋出異常。現在你會把好友加到vector里,但沒有加到數據庫里。
   
    這時候你會質問做數據庫的人們:“為什么你們不返回出錯代碼,而要拋出異常呢?”他們會說:“我們使用的是一個運行在TZN網絡上的高可靠性的集群數據庫服務器,極少出錯。因此,我們認為應該用異常來表示出錯是最好的,因為異常只出現在異常的情況下,不是嗎?”
   
    這個理由講得通,但是你還是要處理出錯情況。你不會希望因為數據庫出錯而導致整個系統一片混亂。這樣你修復數據庫時,不必關閉整個服務器程序。
   
    本質上,你必須做兩個操作,它們中的任何一個都可能失敗。當其中一個失敗時,你必須撤銷全部操作。讓我們來看看怎么做這件事。
   
    ... ...
   
    呵,因為文章不那么短,所以我會慢慢的給你帖出來的,你也不用太心急了:)
 
    * 方法1:粗魯的方法
 
        一個簡單的辦法是在 try-catch 塊中拋出異常   
   
    void User::AddFriend(User& newFriend)
    {
        friends_.push_back(&newFriend);
        try
        {
            pDB_->AddFriend(GetName(), newFriend.GetName());
        }
        catch (...)
        {
            friends_.pop_back();
            throw;
        }
    }
 
    如果vector::push_back失敗,那沒有問題,因為UserDatabase::AddFriend不會被執行。如果 UserDatabase::AddFriend失敗,你捕獲這個異常(不管什么類型),然后你調用vector::pop_back撤銷 push_back的操作,然后再次拋出同樣類型的異常。
   
    這樣的代碼可以工作,代價是增加了代碼量,並顯得臃腫。本來兩行的程序變成了六行。想象一下如果你的代碼到處是這樣的try-catch語句,那太可怕了,所以這個方法一點也不吸引人
   
    而且,這個方法也不好擴展。假如你有三個操作要做,用這個方法寫出來的代碼將臃腫得多。你有兩個差不多壞的寫法可選:用嵌套的try語句,或者用使流程更復雜的附加標志。這個方法會導致很多問題,如代碼膨脹、效率下降,以及最重要的可理解性和可維護性降低。
   
    * 方法2:原則(politically)上正確的方法
   
    如果你把上面的代碼給任何一個C++專家看,它很可能會告訴你:“啊,那方法不好。你應該使用慣用法RAII(Resource Acquisition Is Initialization,資源分配即初始化),在出錯的情況下,依靠析構函數來釋放資源。”
   
    OK,讓我們沿着這條路走下去。對於每一個你需要撤銷的操作,都需要有一個對應的類,這個類的構造函數“做”這個操作,而析構函數“撤銷”這個操作,除非你調用了一個“提交”函數,那樣的話析構函數就什么也不做。
   
    用一些代碼可以把這一切說清楚。對於push_back操作,我們寫一個VectorInserter類,就像這樣:
   
    class VectorInserter
    {
    public:
        VectorInserter(std::vector<User*>& v, User& u)
        : container_(v), user_(u), commit_(false)
        {
            container_.push_back(&u);
        } 
        void Commit() throw()
        {
            commit_ = true;
        }
        ~VectorInserter()
        {
            if (!commit_) container_.pop_back();
        }
    private:
        std::vector<User*>& container_;
        User& user_;
        bool commit_;
    };   
 
    大概上面代碼里最重要的東西就是Commit后面的throw()。它說明了一個事實,就是Commit調用永遠成功(不會拋出異常)。因為你已經完成你的工作——Commit只是告訴VectorInserter:“一切順利,不用恢復任何東西。”
 
    你可以像這樣使用整個機制:
   
    void User::AddFriend(User& newFriend)
    {
        VectorInserter ins(friends_, &newFriend);
        pDB_->AddFriend(GetName(), newFriend.GetName());
   
        // Everything went fine, commit the vector insertion
        ins.Commit();
    }
 
    AddFriend現在有兩個不同的部分:動作階段——完成操作;提交階段——不會拋出異常,只是停止所有的撤銷工作。
   
    AddFriend的工作方式很簡單:如果任何一個操作失敗,那么就不能到達提交點,全部操作都會被取消。VectorInserter會把加入的數據pop_back掉,所以程序會保持在調用AddFriend以前的狀態。
   
    這個方法在所有情況下都工作得很好。比如,當vector插入失敗時,ins的析構函數不會被調用,因為ins沒有被成功構造出來。
   
    這個方法很好,但是在現實世界里,做不到那么簡潔。你必須編寫一大堆很小的類來支持這個方法。額外的類意味着要寫額外的代碼,多費腦筋,以及在你的 class browser里會有更多的條目。而且,你會有更多的地方需要處理異常安全問題。僅僅為了在析構函數里撤銷一個操作而不斷增加新的類,從生產率來看,這不是最聰明的辦法。
   
    哦,還有,VectorInserter里有一個bug,你注意到了嗎?編譯器為你隱式生成的拷貝構造函數會導致錯誤:如果被復制的對象還沒有提交過,那么在以后的析構函數里,就可能做過多的pop_back操作。定義一個類是很困難的,這是我們要避免寫很多類的另一個理由。
   
    ... ...
   
    * 方法3:現實中的方法(自注:這種方法現在用的很普遍的,真的比較符合現實哦)
 
    在現實世界里,當程序員坐下來編寫AddFriend時,要么他看過上面的幾個選擇,要么他沒有時間去關心這些。一天過去后,你知道真實的結果通常是什么嗎?你當然知道:
   
    void User::AddFriend(User& newFriend)
    {
        friends_.push_back(&newFriend);
        pDB_->AddFriend(GetName(), newFriend.GetName());
    }
   
    “誰說過內存會用完?這機器有半G內存呢!”   -- 自注:呵呵,我做電信系統,100 個 G 內存都嫌少:)
   
    “程序會因為沒有內存而崩潰?要那樣的話,內存交換早就讓系統慢得像蝸牛了。”
   
    “做數據庫的那幫家伙說AddFriend幾乎不可能失敗。他們用的是XYZ和TZN!
   
    “這太麻煩了,不值得。以后review的時候再考慮它吧。”
   
    一個方法如果需要很多約束規則和受到抱怨的工作,那么它就不具有吸引力。在進度的壓力下,一個好的但是笨拙的方法會變得不實用。雖然每個人都知道應該按照書本上說的那樣去做,但他們始終都喜歡走捷徑。唯一的方法是提供一個可重用的解決方案,正確而容易使用。
   
    當你走捷徑時,因為你知道你的代碼不完美,你會懷着某種不愉快的心情check in 你的代碼。但是這種心情會逐漸消失,因為所有的測試都可以通過。但是隨着時間推移,那些“理論上”會引起問題的地方,還是會開始從現實中冒出來。
   
    你知道你遇到了問題,而且是個大問題:你放棄了對應用程序正確性的控制。現在,當服務器程序崩潰時,你沒有很多線索去找錯:硬件故障?真正的bug?還是因異常而引起的混亂狀態?你遇到的不是無心的bug,而是你自己故意引入的。
   
    即使在一段時間內程序可以工作,但是事情總是會變化的。用戶數目會增加,導致內存使用到達極限。你的網絡管理員可能為了保證性能而禁止內存分頁系統。你的數據庫可能不是那么可靠。你對這一切都沒有准備。
   
    * 方法4:Petru的方法
   
    用ScopeGuard——我們稍后詳細介紹——你很容易就可以寫出簡潔、正確而高效的代碼:
   
    void User::AddFriend(User& newFriend)
    {
        friends_.push_back(&newFriend);
        ScopeGuard guard = MakeObjGuard(friends_, &UserCont::pop_back);
        pDB_->AddFriend(GetName(), newFriend.GetName());
        guard.Dismiss();
    }
   
    上面代碼里,guard對象唯一的任務就是在它離開作用域時,調用friends_.pop_back,除非你調用了Dismiss。如果你調用了,那么guard就什么也不做
   
    ScopeGuard在它的析構函數里實現自動調用某個全局函數或者成員函數。在有異常的情況下,你會想要實現自動撤銷原子操作的功能,這時候ScopeGuard會很有用。
   
    你可以這樣使用ScopeGuard:如果你希望幾個操作按照“要么全做,要么全不做”的方式工作,你可以在緊接着每個操作后面放一個ScopeGuard,這個ScopeGuard可以取消前面的操作:
   
    friends_.push_back(&newFriend);
    ScopeGuard guard = MakeObjGuard(friends_, &UserCont::pop_back);
   
    ScopeGuard也可用於普通函數:
 
    void* buffer = std::malloc(1024);
    ScopeGuard freeIt = MakeGuard(std::free, buffer);
    FILE* topSecret = std::fopen("cia.txt");
    ScopeGuard closeIt = MakeGuard(std::fclose, topSecret);
   
    當整個原子操作成功時,你Dismiss所有guard對象。否則每個ScopeGuard對象會忠實的調用你構造它時所傳的那個函數。
   
    有了ScopeGuard,你可以簡單的安置各種撤銷操作,而不再需要寫特別的類來做諸如刪除vector的最后一個元素、釋放內存、關閉文件這樣的事情。這使ScopeGuard成為編寫異常安全代碼的一個極其有用、並且可重用的解決方案,它使一切變得很簡單。
   
    * 實現ScopeGuard
   
    ScopeGuard是對C++慣用法RAII(資源分配即初始化)典型實現的一個推廣。它們的區別在於ScopeGuard只關注資源清理的那部分—— 資源分配由你自己做,而ScopeGuard處理資源的釋放(事實上,可以論證清理工作是這個諺語里最重要的部分)。
   
    釋放資源有很多種形式,比如調用一個函數、調用一個functor、或者調用一個對象的成員函數,而每種方式都可能有零個、一個或者更多的參數。
   
    自然,我們通過一個類層次關系來對這些變體建模。層次中各個類的對象的析構函數完成實際工作。層次中的根為ScopeGuardImplBase類,如下:
   
    class ScopeGuardImplBase
    {
    public:
        void Dismiss() const throw()
        { dismissed_ = true; }
    protected:
        ScopeGuardImplBase() : dismissed_(false)
        {}
   
        ScopeGuardImplBase(const ScopeGuardImplBase& other)
            : dismissed_(other.dismissed_)
        { other.Dismiss(); }
    ~ScopeGuardImplBase() {} // nonvirtual (see below why)
   
    mutable bool dismissed_;
   
    private:
        // Disable assignment
        ScopeGuardImplBase& operator=(const ScopeGuardImplBase&);
    };
   
    ScopeGuardImplBase集中了對dismissed_標志的管理,這個標志控制派生類是否要執行清理工作。如果dismissed_為真,則派生類在他們的析構函數里什么也不做。
   
    現在我們來看看ScopeGuardImplBase析構函數定義里缺少的virtual。如果析構函數不是virtual的,你怎么可以期望析構函數有正確的多態行為呢?好,把你的好奇心再保持一會兒,我們手里還有張王牌,我們可以通過它得到多態的析構行為,而不必付出虛函數的代價。
   
    現在我們先來看看怎么實現這樣一個對象,它在析構函數里調用一個帶一個參數的函數或者functor。然而當你調用了Dismiss,那么這個函數或者functor就不會被調用。
   
    template <typename Fun, typename Parm>
    class ScopeGuardImpl1 : public ScopeGuardImplBase
    {
    public:
        ScopeGuardImpl1(const Fun& fun, const Parm& parm)
        : fun_(fun), parm_(parm)
        {}
   
        ~ScopeGuardImpl1()
        {
            if (!dismissed_) fun_(parm_);
        }
    private:
        Fun fun_;
        const Parm parm_;
    };
   
    為了方便使用ScopeGuardImpl1,我們寫一個輔助函數。
   
    template <typename Fun, typename Parm>
        ScopeGuardImpl1<Fun, Parm>
    MakeGuard(const Fun& fun, const Parm& parm)
    {
        return ScopeGuardImpl1<Fun, Parm>(fun, parm);
    }
   
    MakeGuard依靠編譯器推導出模板函數中的模板參數,這樣你就不用自己指定ScopeGuardImpl1的模板參數了——事實上你不需要顯式創建 ScopeGuardImpl1對象。這個技巧也被一些標准庫中的函數所使用,如make_pair和bind1st。
   
    你還對不使用虛構造函數而得到多態性析構行為的方法感到好奇嗎?下面是ScopeGuard的定義,會讓你大吃一驚的是,它僅僅是一個typedef:
   
    typedef const ScopeGuardImplBase& ScopeGuard;
   
    好了現在讓我們來揭開全部神秘機制。根據C++標准,如果const的引用被初始化為對一個臨時變量的引用,那么它會使這個臨時變量的生命期變得和它自己一樣。讓我們舉個例子來解釋這件事。如果你寫:
   
    FILE* topSecret = std::fopen("cia.txt");
    ScopeGuard closeIt = MakeGuard(std::fclose, topSecret);
   
    那么MakeGuard創建了一個臨時變量,它的類型為(看以前做一下深呼吸):
    ScopeGuardImpl1<int (&)(FILE*), FILE*>
   
    這是因為std::fclose是接受FILE*類型參數返回int的函數。具有上面那個類型的臨時變量被指派給了const引用closeIt。根據上面提到的C++語言規則,這個臨時變量會和它的引用closeIt有同樣長的生存期——當這個臨時變量被析構時,會調用正確的析構函數。接着,析構函數關閉文件。
   
    ScopeGuardImpl1支持有帶參數的函數(或functor)。很容易就可以寫出不帶參數、帶兩個參數或帶更多參數的類(ScopeGuardImpl0、ScopeGuardImpl2??)。當你有了這些類,你就可以重載MakeGuard,從而得到一個優美、統一的語法:
   
    template <typename Fun>
        ScopeGuardImpl0<Fun>
    MakeGuard(const Fun& fun)
    {
        return ScopeGuardImpl0<Fun >(fun);
    }
   
    ...
   
    到現在為止,我們已經有了一個強大的工具來表達調用一組函數的原子操作。MakeGuard是一個優秀的工具,特別是它同樣可以用於C語言的API,而不需要寫很多包裝類。
   
    更好的是,它不損失效率,因為它不涉及到虛函數調用。
   
    * 針對對象和成員函數的ScopeGuard
   
    到現在為止,一切都很好,但是怎么調用對象的成員函數呢?其實這一點也不難。讓我們來實現ObjScopeGuardImpl0,一個可以調用對象的無參數成員函數的類模板。
   
    template <class Obj, typename MemFun>
    class ObjScopeGuardImpl0 : public ScopeGuardImplBase
    {
    public:
        ObjScopeGuardImpl0(Obj& obj, MemFun memFun)
        : obj_(obj), memFun_(memFun)
        {}
   
        ~ObjScopeGuardImpl0()
        {
            if (!dismissed_) (obj_.*fun_)();
        }
   
    private:
        Obj& obj_;
        MemFun memFun_;
    };
   
    ObjScopeGuardImpl0有一點特別,因為它用了不太為人所知的語法:指向成員函數的指針和operator.*()。為了理解它是如何工作的,讓我們來看看MakeObjGuard的實現(我們在本節開始已經利用過MakeObjGuard了)。
   
    template <class Obj, typename MemFun>
        ObjScopeGuardImpl0<Obj, MemFun, Parm>
    MakeObjGuard(Obj& obj, Fun fun)
    {
        return ObjScopeGuardImpl0<Obj, MemFun>(obj, fun);
    }
   
    現在,如果你調用:
   
    ScopeGuard guard = MakeObjGuard(friends_, &UserCont::pop_back);
   
    會創建一個如下類型的對象:
   
        ObjScopeGuardImpl0<UserCont, void (UserCont::*)()>
   
    幸好,MakeObjGuard讓你免於寫那些跟字符型圖標一樣單調的類型。工作機制還是一樣——當guard離開作用域,臨時對象的析構函數會被調用。析構函數通過指向成員的指針調用成員函數。這里我們用到了.*操作符。
   
    * 錯誤處理
   
    如果你讀過Herb Sutter關於異常的著作[2],你就會知道一條基本原則:析構函數不應該拋出異常。一個會拋出異常的析構函數會讓你無法寫出正確的代碼,並且會再沒有任何警告的情況下終止你的應用程序。在C++里,當一個異常被拋出,在堆棧展開(unwinding)的時候某個析構函數又拋出另一個異常,應用程序會被馬上終止。
   
    ScopeGuardImplX和ObjScopeGuardImplX分別調用了一個未知的函數或成員函數,那個函數可能會拋出異常。這會終止程序,因為我們設計guard的析構函數的目的就是:當有異常被拋出,在展開(unwinding)堆棧時,調用這個未知函數!理論上,你不應該把可能拋出異常的函數傳給MakeGuard或者MakeObjGuard。在實用中(你可以從供下載的代碼中看到),析構函數對異常采取了防御措施。
 
    template <class Obj, typename MemFun>
    class ObjScopeGuardImpl0 : public ScopeGuardImplBase
    {
        ...
   
    public:
        ~ScopeGuardImpl1()
        {
            if (!dismissed_)
            try { (obj_.*fun_)(); }
            catch(...) {}
        }
    }
 
    是的,catch(...)塊什么事也不做。這可不是隨手寫的,這在異常處理的領域中是很基本的:如果你的“撤銷/恢復”操作也失敗了,那么你幾乎沒有什么事情可以做了。你嘗試恢復,但是不管恢復操作是否成功,你都應該繼續下去。
   
    以我們的即時消息為例,一個可能動作順序是:你向數據庫里加入了一個好友數據,但當把它插入friends_ vector時失敗了,當然你會嘗試把它從數據庫里再刪掉。雖然可能性很小,但是從數據庫里刪除數據時,不知道為什么也失敗了,這種情況就很討厭了。
   
    一般來說,你應該在那些保證可以成功撤銷的操作上使用guard
   
    * 支持傳引用的參數
   
    在Petru和我很高興地使用ScopeGuard一段時間以后,我們遇到一個問題。考慮下面的代碼:
    void Decrement(int& x) { --x; }
    void UseResource(int refCount)
    {
        ++refCount;
        ScopeGuard guard = MakeGuard(Decrement, refCount);
    }
   
    上面代碼中的guard對象確保refCount的值在UseResource函數退出時保持不變。(這個慣用法在一些共享資源的情況下很有用。)
   
    盡管有用,但上面的代碼不能工作。問題在於,ScopeGuard保存了refCount的一個拷貝(看一下ScopeGuardImpl1的定義,在成員變量parm_里)而不是對它的引用。然而在這個例子里,我們需要的是保存refCount的一個引用,這樣才能讓Decrement對它進行操作。
 
    一個解決辦法是再實現一些類,例如ScopeGuardImplRef,以及MakeGuardRef。這會有很多重復勞動,並且在實現處理多參數的類時,這個辦法就很難應付了。
 
    我們采取的辦法是:使用一個輔助類,它把引用轉變為一個值。
 
    template <class T>
    class RefHolder
    {
        T& ref_;
   
    public:
        RefHolder(T& ref) : ref_(ref) {}
        operator T& () const
        {
            return ref_;
        }
    };
 
    template <class T>
    inline RefHolder<T> ByRef(T& t)
    {
        return RefHolder<T>(t);
    }
 
    RefHolder以及和它配套的輔助函數ByRef可以無縫地使引用適合於值的語義,並且使ScopeGuardImpl1不需要任何改變就可以使用引用。你要做的只是把引用形式的參數用ByRef包裝一下,就象這樣:
 
    void Decrement(int& x) { --x; }
    void UseResource(int refCount)
    {
        ++refCount;
        ScopeGuard guard = MakeGuard(Decrement, ByRef(refCount));
        ...
    }
       
    我們發現這個方法很有說明性,它提醒你正在用引用方式傳遞參數。
 
    這個支持引用的辦法最好的一點是在ScopeGuardImpl1中的const修飾。這里是相關的代碼摘要:
 
    template <typename Fun, typename Parm>
    class ScopeGuardImpl1 : public ScopeGuardImplBase
    {
        ...
    private:
        Fun fun_;
        const Parm parm_;
    };
 
    這個小小的const非常重要。它防止使用非const引用的代碼通過編譯和不正確地運行。換句話說,如果你忘記使用ByRef,編譯器不會讓這樣的錯誤代碼通過。
 
    * 再等一下,還有一點
   
    到現在為止,你有了一個好工具可以幫助你寫出正確的代碼,而不用發愁。然而有時候你會想要ScopeGuard在退出一個代碼塊時始終執行。這種情況下,定義一個ScopeGuard類型的啞變量很麻煩——你只需要一個臨時變量,而不需要給它命名。
   
宏ON_BLOCK_EXIT可以做到這樣,你可以這樣寫出表達力更好的代碼:
   
    {
    FILE* topSecret = fopen("cia.txt");
    ON_BLOCK_EXIT(std::fclose, topSecret);
    ... use topSecret ...
    } // topSecret automagically closed
   
    ON_BLOCK_EXIT表示:“我希望在當前代碼塊退出時做這個動作。”類似的,ON_BLOCK_EXIT_OBJ對於成員函數調用實現相同的功能。
   
    這些宏使用了不太正統的(雖然合法的)花招,這里就不公開了。如果你好奇,你可以到代碼里去查看這些宏(因為編譯器的bug,用Microsoft VC++的朋友必須關閉“Program Database for Edit and Continue”設定,否則ON_BLOCK_EXIT會有問題)。
   
    * 現實中的ScopeGuard
   
    我們喜歡ScopeGuard是因為它易於使用和概念簡單。這篇文章詳細講了整個實現,但是要解釋ScopeGuard的用法只要幾分鍾。 ScopeGuard在我們的同事中間象野火一樣迅速傳播開來,每個人都認為它是一個很有價值的工具,很多情況下有助於防止因為異常而過早返回。有了 ScopeGuard,你可以輕松地編寫異常安全的代碼,而且理解和維護也同樣簡單。
   
    每個工具都有推薦的使用方法,ScopeGuard也不例外。你應該象ScopeGuard期望的那樣使用它——作為函數中的自動變量。你不應該把 ScopeGuard對象用作成員變量,或者在堆上分配它們。為此,供下載的代碼中包含了一個Janitor類,它和ScopeGuard做的事情一樣,但是采取了更通用的做法——代價是損失了一些效率。因為編譯器的bug,Borland C++ 5.5的用戶需要使用Janitor而不是ScopeGuard。
   
    * 結論
   
    我們討論了一些在編寫異常安全代碼中出現的一些情況。在比較了幾個在這些情況下獲得異常安全性的方法以后,我們介紹了一個方法,適用於有防錯性(並且不會再throw)撤銷操作可用的情況。ScopeGuard使用了若干泛型編程的技術,讓你指定在ScopeGuard退出代碼塊時調用的函數和成員函數。作為可選項,你也可以解除ScopeGuard對象的動作。
   
    當你需要實行資源的自動釋放,並且可以依靠防錯的撤銷操作,ScopeGuard在着這種情況下對你很有幫助。當你把幾個可能會失敗,但是也可以撤銷的操作組成一個原子操作,這個慣用法就變得很重要了。當然這個方法也有不適用的情況。
 
 
 
上述文章中實現Scope Guard時使用的兩個比較精巧的用法:一個是利用臨時對象綁定到引用來獲取無需虛擬機制開銷的多態效果。另一個是如何對值的進行引用適配。

先看第一個:利用臨時對象綁定到引用來獲取無需虛擬機制開銷的多態效果

class ScopeGuardImplBase
{
private:
    /// Copy-assignment operator is not implemented and private.
    ScopeGuardImplBase& operator =(const ScopeGuardImplBase&);

protected:

    ~ScopeGuardImplBase()
    {}

    /// Copy-constructor takes over responsibility from other ScopeGuard.
    ScopeGuardImplBase(const ScopeGuardImplBase& other) throw()
        : dismissed_(other.dismissed_)
    {
        other.Dismiss();
    }

    template <typename J>
    static void SafeExecute(J& j) throw()
    {
        if (!j.dismissed_)
            try
            {
                j.Execute();
            }
            catch(...)
            {}
    }

    mutable bool dismissed_;

public:
    ScopeGuardImplBase() throw() : dismissed_(false)
    {}

    void Dismiss() const throw()
    {
        dismissed_ = true;
    }
};

typedef const ScopeGuardImplBase& ScopeGuard;

template <typename F, typename P1>
class ScopeGuardImpl1 : public ScopeGuardImplBase
{
public:
    static ScopeGuardImpl1<F, P1> MakeGuard(F fun, P1 p1)
    {
        return ScopeGuardImpl1<F, P1>(fun, p1);
    }

    ~ScopeGuardImpl1() throw()
    {
        SafeExecute(*this);
    }

    void Execute()
    {
        fun_(p1_);
    }

protected:
    ScopeGuardImpl1(F fun, P1 p1) : fun_(fun), p1_(p1)
    {}

    F fun_;
    const P1 p1_;
};

template <typename F, typename P1>
inline ScopeGuardImpl1<F, P1> MakeGuard(F fun, P1 p1)
{
    return ScopeGuardImpl1<F, P1>::MakeGuard(fun, p1);
}

    這里最巧妙的是MakeGuard函數返回一個臨時變量,而關於臨時對象又有:

  1. C++中保證如果臨時對象被初始化給一個引用(指針好像不行),那么臨時變量的生命期和該引用的生命期一樣。
  2. C++保證臨時對象一定會被正確的析構(而不管引用這個臨時對象的那個引用的類型),這個正確的析構正式我們利用的地方,編譯器知道(或者說記住了)該臨時對象的實際類型,所以會正確的調用該對象的析構函數。想一下如果不是臨時對象,而我們通過基類的引用去引用一個派生類對象,個人認為在這種情況下,編譯器是不會記住該對象的動態類型的,而是通過虛擬機制來找到正確的析構函數,所以如果基類的析構函數不聲明為virtual的話,只有基類的構造函數會被調用。

    所以如果我們通過派生類的臨時對象來初始化一個靜態類型為基類的引用,我們將獲得只有在虛擬機制下才獲得的正確的析構行為。

再來看看第二點:如何對值的進行引用適配

    如果一個模板函數接受的是一個變量而不是引用作為參數,並且函數有可能會修改這個變量的值(我們先把該函數在對參數有副作用的時候為什么不用引用作為參數這個疑問放在一邊),如何使該副作用能真正起作用?看代碼:

template <class T>
class RefToValue
{
public:

    RefToValue(T& ref) : ref_(ref)
    {}

    RefToValue(const RefToValue& rhs) : ref_(rhs.ref_)
    {}

    operator T& () const
    {
        return ref_;
    }

private:
    // Disable - not implemented
    RefToValue();
    RefToValue& operator=(const RefToValue&);

    T& ref_;
};

template <class T>
inline RefToValue<T> ByRef(T& t)
{
    return RefToValue<T>(t);
}

使用上述這個輔助類就能解決這個問題,只要在傳參數的時候使用ByRef(參數)就行了:

#include<iostream>
using namespace std;

template <typename T>
void f(T t)
{
    ++t;
}

int main()
{
    int k = 100;
    cout << "before function calling: k = " << k << endl;
    f(k);
    cout << "after the first function calling: k = " << k << endl;
    f(ByRef(k));
    cout << "after the sencond function calling: k = " <<  k << endl;
    return 0;
}

關於域守衛的具體實現,大家看了上述鏈接中的文章應該都知道了!當然最好看看Loki的源代碼。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM