有效創建一個類(四)

在前三篇中我說明了有效創建一個類的前4個考慮步驟,現在就差最後一步了,考慮創建與類定義有關的異常類。 

異常的概述 

用戶調用某個函數,函數可以在運行時檢測到錯誤,但是不知道如何處理;用戶呢,實際上知道在遇到這種錯誤時,該如何處理;爲了解決這類問題,提出了異常的概念。異常的基本思想是:當函數檢測到自己無法處理的錯誤時拋出一個異常,以便調用者(用戶)能夠處理這個異常。用戶如果希望處理這種異常可以使用catch捕獲這個異常。 

傳統的錯誤處理方式 
(1)終止程序 
(2)返回一個表示“錯誤”的值 
(3)返回一個合法值,讓程序處於某種非法狀態 
(4)調用一個預先準備好,在出現“錯誤”的情況下用的函數 
(1)的方式,在未捕獲異常的情況,默認發生的事情,換句話說,跟沒有異常一樣。但是我們可以做的更好不是嗎?(2)調用者需要檢查錯誤值,這容易使程序的體積加大,而且能夠正常返回錯誤值是前提,但有時並不如人所願。(3)事實上,即使能返回一個合法值,但是調用者往往很難注意到程序已經處在非法狀態中了。(4)看上去與異常處理相似,但是是不是出現這種錯誤時,就一定要執行這個預先準備好的函數呢? 

一個異常就是某個用於表示異常發生類的一個對象。檢查到一個錯誤的代碼段throw一個對象。一個catch語句表明它要處理某個異常。一個throw的作用就表示堆棧的一系列回退,直到找到適當的catch。 

異常與資源管理 
對於一個資源管理類來說,一旦在獲取資源的過程中,或者使用資源的過程中,捕獲到異常,需要釋放掉自身佔有的異常。當然可以使用try, catch語句,但是C++提出的“資源申請即初始化”的方式更優雅,更安全。 

異常與new 
一旦在使用new時,捕獲到異常,那麼處理異常的代碼中必須調用對應的delete。 

異常與資源耗盡 
當堆內存由於申請的空間過大,已經耗盡資源時;C++默認調用_new_handler函數指針。 

C++代碼 
  1. void customized_new_handler(){  
  2.   ...  
  3. };  
  4.   
  5. set_new_handler(&customize_new_handler); //std func  
  6.   
  7. void f()  
  8. {  
  9.    void (*oldnh)() = set_new_handler(&customized_new_handler);  
  10.    try {  
  11.      // ...  
  12.    }  
  13.    catch(bad_alloc) {  
  14.      // ...  
  15.    }  
  16.    catch( ... ){  
  17.       set_new_handler(oldnh); //reset handler  
  18.       throw//re-throw  
  19.    }  
  20.      
  21.    set_new_handler(oldnh); //reset handler  
  22. }  

上述是資源耗盡的一般情況,極端情況可能連new一個異常對象的空間也沒有了,那怎麼辦?不用擔心,C++語言已經幫我們想好了,那就是每一個C++程序實現都要求保留足夠的存儲,在資源耗盡的情況,仍然可以拋出bad_alloc。 

異常與構造函數 
因爲構造函數其特殊性,無法返回一個獨立的值供調用程序檢查。異常處理機制允許從構造函數內部傳出來錯誤信息。通常構造函數多與資源申請有關,所以建議採用“資源申請即初始化”的方式處理異常。 

異常與成員初始化 
將成員初始式包含在try,catch塊內。 
C++代碼 
  1. class X {  
  2.   Vector v;  
  3.   //  
  4. public:  
  5.   X(int);  
  6.   //  
  7. };  
  8.   
  9. X::X(int s)  
  10. try  
  11.       :v(s)  
  12. {   
  13.    // ...  
  14. }  
  15. catch(Vector::Size){  
  16.   // ...  
  17. }  


異常與析構函數 
析構函數的調用存在兩種情況 
I.  正常銷燬對象,調用 
II. 因異常,在捕獲異常的處理塊中調用; 
對於後一種情況,絕不能讓析構函數裏拋出異常。如果真是這樣,那就是異常處理機制的一次失敗,並調用std::terminate()。那麼拋出異常退出析構函數也違背了標準庫的要求。 
如果析構函數必須要調用一個可能拋出的異常的函數,可以通過try, catch塊保護自己;當然保護方式要麼吞掉所有可能拋出的異常,要麼終止程序。如果要求析構函數調用的這個可以拋出異常的函數必須做出運行時異常反應的話,那麼請考慮使用一個普通函數。(參考《Effective C++》條款08) 

異常的描述 
C++代碼 
  1. void f() throw( x2, x3); //may throw only x2, x3 exceptions  
  2. void f()                 //can throw any exception  
  3. void f() throw();        //no exception thrown   


異常的映射 
unexpected()的行爲與std::bad_exception映射; 
與此set_new_handler()類似,對unexpected()的響應由_unexpected_handler決定,它又是通過<exception>中的std::set_unexpected()設置的。 

用戶自定義的異常映射 
C++代碼 
  1. void g() throw(Yerr);  

g()被在網絡分佈式環境下被調用,由於g()對網絡異常一無所知,自然調用unexpected()。如果不希望g()調用unexpected(),那麼需要g()處理所有網絡情況可能拋出的那些網絡異常,那麼就需要重寫g()。如果g()由於種種限制不能重寫,我們只能通過重新定義unexpected()完成。 

一、首先採用“資源申請即初始化”方式爲unexpected()函數定義一個類 
C++代碼 
  1. //摘自《The C++ Programming Language》第14.6.3節  
  2.   
  3. typedef void(*unexpected_handler)();  
  4. unexpected_handler set_unexpected(unexpected_handler);  
  5.   
  6. class STC { //store and reset class  
  7.   unexpected_handler old;  
  8. public:  
  9.   STC(unexpected_handler f){ old = set_unexpected(f);}  
  10.   ~STC(){ set_unexpected(old);}  
  11. };  


二、定義一個函數,使它具有我們希望的unexpected()的意義 
C++代碼 
  1. class Yunexpected:public Yerr{};  
  2. void throwY() throw (Yunexpected) { throw Yunexpected(); }  


三、提供一個網絡版g函數 
C++代碼 
  1. void networked_g() throw (Yerr)  
  2. {  
  3.    STC xx(&throwY);  
  4.    g();  
  5. }  

但是上述對於用戶而言,只是知道因爲調用g()產生一個unexpected異常,具體是什麼網絡異常並不知道,那麼怎麼辦呢? 
這時修改下Yunexpected的類定義和throwY()的定義,使之可以保存真正的異常信息即可。 
C++代碼 
  1. class Yunexpected:public Yerr{  
  2. public:  
  3.   Network_exception * pne;  
  4.   Yunexpected(Network_exception *p) : pne(p?p->clone():0){}  
  5.   ~Yunexpected(){delete pne;}  
  6. };  
  7.   
  8. void throwY() throw(Yunexpected){  
  9.   try{  
  10.     throw//re-throw  
  11.   }  
  12.   catch(Network_exception& p){  
  13.     throw Yunexpected(&p);  
  14.   }  
  15.   catch( ... ){  
  16.     throw Yunexpected(0);  
  17.   }  
  18. }  


未捕獲異常 
缺省情況下,如果拋出一個異常未被捕獲,那就會調用函數std::terminate()。 
uncaught_exception由_uncaught_handler決定,uncaught_handler由std::set_terminate()設置。 

標準異常體系 
標準異常體系的樣子如下圖所示 
 
其中exception在文件<exception>裏給出 
C++代碼 
  1. class exception{  
  2. public:  
  3.   exception() throw ();  
  4.   exception(const exception&) throw();  
  5.   exception& operator=(const exception&) throw();  
  6.   virtual ~exception() throw();  
  7.   virtual const char* what() const throw();  
  8. private:  
  9.   // ...  
  10. };  

所有標準異常都由exception派生,然後不是所有的異常都由exception派生,所以通過捕捉exception想捕獲所有異常是錯誤的想法。 

如何定義一個完善的異常類,並且繼承於std::exception體系呢? 
C++代碼 
  1. #include <string>  
  2. #include <exception>  
  3.   
  4. class MyBaseException : public std::exception  
  5. {  
  6. public:  
  7.     //Constructor without inner exception  
  8.     xxBaseException(const std::string& what = std::string("xxBaseException"))  
  9.         : xx_BaseException(0), xx_What(what) {}        
  10.   
  11.     //Constructor with inner exception  
  12.     xxBaseException(const xxBaseException& innerException, const std::string& what = std::string("xxBaseException"))  
  13.         : xx_BaseException(innerException.clone()), xx_What(what) {}  
  14.     
  15.     template <class T>  // valid for all subclasses of std::exception  
  16.     xxBaseException(const T& innerException, const std::string& what = std::string("xxBaseException"))  
  17.         : xx_BaseException(new T(innerException)), xx_What(what) {}  
  18.   
  19.     virtual ~xxBaseException() throw()  
  20.         { if(xx_BaseException) { delete xx_BaseException; } }   
  21.           //don't forget to free the copy of the inner exception  
  22.     const std::exception* base_exception() { return xx_BaseException; }  
  23.     virtual const char* what() const throw()  
  24.         { return xx_What.c_str(); }   
  25.     //add formated output for your inner exception here  
  26. private:  
  27.     const std::exception* xx_BaseException;  
  28.     const std::string xx_What;  
  29.     virtual const std::exception* clone() const  
  30.         { return new xxBaseException(); }   
  31.     // do what ever is necesary to copy yourselve  
  32. };  

上述的自定義xxBaseException還是比較簡單的,還可以自己添加文件名,行號等信息,stackTrace深度等信息。 

雖然在C++裏提供了這樣完備的異常處理機制,(似乎每種語言的異常處理機制大致相同,比如JAVA,C#),但是想象一下如果一個函數或者一個類中佈滿了這樣的try, catch塊,總歸顯得代碼十分醜陋與笨拙。醜陋的同時也大大降低了開發人員對代碼穩定性以及執行效率的自信。 

那麼怎麼才能在避免寫很多try,catch塊的前提下,又能寫出異常安全類或者方法呢? 
C++的實現者,Bjarne Stroustrup,和《Effective C++》的作者,Scott Meyers給了我們關於如何寫異常安全類的建議: 

實現”異常安全“類 
說到異常安全類,那麼其定義是必須要說的。(定義引自"Exception Safty:Concepts and Techniques", Bjarne Stroustrup, Advances in Exception Handling Techniques, Lecture Notes in Computer Science 2022. Springer-Verlag, 2001, 60--76, Springer-Verlag

引用

An operation on an object is said to be exeception safe if that operation leaves the object in a valid state when the operation is terminated by throwing an exception. 


這段定義中需要注意這麼幾個關鍵字: 
1.valid state:合法狀態,合法狀態可以是一個需要清除工作的錯誤狀態,但這個錯誤狀態一定是被良好定義的。這裏良好定義意味着對象擁有合理的錯誤處理代碼。 
2.object:爲了合理地定義合法狀態,對象應該擁有一個invariant,一旦它的構造函數們確立了這個invariant,接下來針對這個對象的所有操作都會維持這個invariant,直到析構函數完成最後的清除工作。這段話裏涉及到了一個invariant關鍵字,在維基百科裏關於invariant的定義給出了兩類,一類稱爲class invariant, 另一類是object invariant。這兩個概念對於我們更好地理解如何“合理”定義“合法”狀態有十分大的幫助。 
3.throwing an exception: 操作是因爲有異常拋出而終止的,object state也是針對這種情境下而言的。 

class invariant 和 object invariant的簡單介紹 

這裏我給出原文的原因是至今我沒有找到非常貼切的術語來描述class invariant,雖然我可能理解了這個定義。 

class invariant: A class invariant is an invariant used to constrain objects of a class. Methods of the class should preserve the invariant. The class invariant constrains the state stored in the object. 
(類不變量:類的約束條件,約束了存儲在對象內的狀態。) 
Class invariants are established during construction and constantly maintained between calls to public methods. 
(狹隘理解:這個類約束條件主要針對類管理的資源,無論是內存還是文件句柄或者數據庫連接等等,只要資源在構造函數中被確立,那麼後續的任何類對象操作,這個約束條件始終有效且保持不變。例如一個vector,類約束條件是在堆中申請下來) 
object invariant: is a programming construct consisting of a set of invariant properties that remain uncompromised regardless of the state of the object. This ensures that the object will always meet predefined conditions, and that methods may, therefore, always reference the object without the risk of making inaccurate presumptions. 
(對象不變量:是由一組無論對象狀態如何都不妥協地保持不變的屬性組成的編程概念。這確保了對象一直滿足預定義的條件) 

基於此,合法狀態的定義對於異常安全類顯得至關重要了;所以“合理狀態”給出了三個保證 
1. 基本保證:類不變量基本被保持,至少類在構造期確定下來的資源(內存,數據庫連接,SOCKET,文件句柄等)不會有泄漏。 
2. 強烈保證:類似於數據庫的事務概念。針對類的關鍵操作,在基本保證的前提下要麼操作完全成功,要麼完全失敗,無中間狀態可言。 
3. 不拋擲保證:在基本保證的前提下,對一些操作保證不拋擲異常。 

那麼實現異常安全類,基於不同的保證有一些編程技巧如下: 
(1)try塊 
(2)資源申請即初始化 
其中資源申請即初始化這個點子的關鍵之處就是使資源的擁有者賦予一個局域化的對象。那麼,在大多數情況,通過這個局域化對象的構造函數構建這個資源,當這個局域化對象被銷燬時,其管理的資源自然地通過析構函數銷燬掉,無論析構函數的調用時正常調用還是因爲拋出異常調用。這樣就保證了資源不會泄漏。 

C++代碼 
  1. //摘自《Effective C++》條款29  
  2. class PrettyMenu {  
  3. public:  
  4.   ...  
  5.   void changeBackground(std::istream& imgSrc);  
  6.   ...  
  7. private:  
  8.   Mutex mutex;  
  9.   Image* bgImage;  
  10.   int imageChanges;  
  11. };  
  12.   
  13. void PrettyMenu::changeBackground(std:;istream& imgSrc){  
  14.   lock(&mutex);  
  15.   delete bgImage;  
  16.   ++imageChanges;  
  17.   bgImage = new Image(imgSrc);  
  18.   unlock(&mutex);  
  19. }  


上述的代碼顯然有很多問題: 
【1】資源泄漏:如果new Image(imgSrc)因爲imgSrc給的源不正確而拋出異常,那麼unlock語句不會被執行,於是鎖永遠不會被釋放,造成資源泄漏; 
【2】數據損壞:如果new Image(imgSrc)失敗,拋出異常,那麼bgImage就會指向一個已經被delete掉的對象,imageChanges也會被累加,而我們知道這違背事實。 

所以上述代碼可以這樣改變下以解決上面兩個問題 

C++代碼 
  1. class PrettyMenu {  
  2.   ...  
  3.   std::tr1::shared_ptr<Image> bgImage;  
  4.   ...  
  5. };  
  6.   
  7. void PrettyMenu::changeBackground(std::istream& imgSrc){  
  8.   Lock ml(&mutex);  
  9.   bgImage.reset(new Image(imgSrc));  
  10.   ++imageChanges;  
  11. }  

上述代碼中,mutex資源的擁有者賦予給了一個局域化Lock對象ml。這樣通過Lock類的構造函數確立了資源的獲取,即獲得鎖;如果函數成功執行完後,函數內局域變量ml被自動銷燬。銷燬時,通過Lock類的析構函數自動釋放鎖;即使拋出異常時,由於銷燬函數,所以鎖同樣也會被釋放。 

std::tr1::shared_ptr<T>是一個智能指針,通過智能指針的reset函數,不再需要手動刪除舊bgImage對象,因爲智能指針內部已經處理掉了,並且處理就圖像的動作永遠依據於new Image(imgSrc)語句的結果。 

上述的代碼已經縮短了函數changeBackground的長度,看起來更精練些。似乎也提供了強烈保證;但是美中不足的是imgSrc這個參數。如果Image構造函數拋出異常,istream的讀取記號已被移走,而這樣就跟該函數執行前有不同的地方了。所以上述changBackground實際只給出了基本保證。那麼怎麼改,才能給出強烈保證呢?考慮可以採用有效創建一個類(三)中的copy and swap方式。這種方式的基本思想就是爲打算修改的對象創建一個副本,然後在副本上做一切必要修改。若有任何修改動作拋出異常,原對象仍保持未改變狀態。待所有改動都成功後,再將修改過的那個副本和原對象在一個不拋出異常的操作中置換。 

實現上通常是將所有“隸屬對象的數據”從原對象放進另一個對象內,然後賦予原對象一個指針,指向那個所謂實現對象(即副本,因爲副本完成修改動作)。這種手法通常稱pointer to implementation, pimpl idiom。 所以就有了如下的可以提供強烈保證的修正代碼 
C++代碼 
  1. struct PMImpl {  
  2.   std::tr1::shared_ptr<Image> bgImage;  
  3.   int imageChanges;  
  4. };  
  5.   
  6. class PrettyMenu {  
  7.   ...  
  8. private:   
  9.   Mutex mutex;  
  10.   std::tr1::shared_ptr<PMImpl> pImpl;  
  11. };  
  12.   
  13. void PrettyMenu::changeBackground(std::istream& imgSrc){  
  14.   using std::swap;  
  15.   Lock ml(&mutex);  
  16.   std::tr1::shared_ptr<PMImpl> pNew(new PMImpl(*pImpl)); //create copy   
  17.   pNew->bgImage.reset(new Image(imgSrc));   //modify copy  
  18.   ++pNew->imageChanges;  
  19.   
  20.   swap(pImpl, pNew); //swap   
  21. }  
發佈了37 篇原創文章 · 獲贊 1 · 訪問量 6萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章