C++華麗的exception handling(異常處理)背後隱藏的陰暗面及其處理方法

 

前言

最近在看auto_ptr源碼的時候,發現裏面的異常說明很多。事實上對於exception handling這塊,以前也有很多困惑的地方,只是由於平時代碼中很少用到,於是就從來沒仔細鑽研過。本來這篇是用來寫smart pointer的,既然遇到了exception handling這塊,那麼先把這塊硬骨頭啃下來再說吧。

翻閱了很多大師的經典著作,發現exception handling在《c++ primer》中只是概念性的提了下,對於技巧型的內容幾乎沒有涉及到;《effective c++》中只有一個條款中提及,《inside c++ object model》也提到很少,幸運的是《more effective c++》中卻有一個專題來研討這塊,而且講到很多技巧性的內容,令人膾炙人口。。 雖然新公司裏很少用到exception handling,所有的狀態信息都是以日誌文件形式來記錄,而誰也不能保證以後的工作中不會用到,對於一般性的異常處理,自認爲還是可以應付的來的,至少不會導致因爲沒有處理的exception而teminate了當前程序,而如果要寫出高質量高穩定性的C++代碼,不掌握exception handling的技巧性使用應該是很難的(至少我是這麼認爲的),當然了,C陣營中錯誤代碼或返回狀態信息是另一類exception技能了。而正如Scott Meyers在《effective c++》的條款一所說:視C++爲一個語言聯邦;因而不該因爲C++是由C發展而來而全盤忽視了C++的特性。。。

 

VS編譯器對異常規範的忽視

異常規範在《C++ Primer》中倒是提及的多一些。這一塊也是exception handling中最令人頭大的一塊,因爲儘管編譯器能檢測出少量異常規範,對於大多數的異常規範,只有在運行期才能知曉,如果違反了異常規範,諸多大師的一致解釋是:程序會自動調用標準庫的unexpacted函數,此函數又調用teminate函數,從而直接終止程序的運行。來看看如下代碼:

  1. #include <iostream> 
  2.  
  3. using namespace std; 
  4.  
  5.  
  6. typedef void (*CallBackPtr)(int nEventLocationX,int nEventLocationY,void *pDataToPassValue)throw(); 
  7.  
  8. class CallBack 
  9. public
  10.     CallBack(CallBackPtr fPtr,void *pDataToPassValue):func(fPtr),pData(pDataToPassValue) 
  11.     {} 
  12.     void MakeCallBack(int nEventLocationX,int nEventLocationY) const throw() 
  13.     { 
  14.         func(nEventLocationX,nEventLocationY,pData); 
  15.     }; 
  16.  
  17. private
  18.     CallBackPtr func; 
  19.     void *pData; 
  20. }; 
  21.  
  22. void MyFunc(int nEventLocationX,int nEventLocationY,void *pDataToPassValue) throw(runtime_error) 
  23.     cout<<nEventLocationX<<" "<<nEventLocationY<<endl; 
  24.     throw runtime_error("runtime error example!"); 
  25.  
  26. int main(int *argc , char **argv) 
  27.     CallBackPtr Func = MyFunc; 
  28.      
  29.     CallBack MyCallBack(Func,NULL); 
  30.     MyCallBack.MakeCallBack(10,10); 
  31.     return 0; 

這是一個回調函數管理類的例子,是《more effective c++》的原例,如果按照Lippman在《C++ Primer》中所說的話,此程序應該不能通過編譯,因爲它存在一個編譯期就能檢測出來的違反異常規範的地方:對於函數MyFunc和函數定義CallBackPtr異常聲明,MyFunc的規範更爲嚴格,它應該不能轉化爲CallBackPtr的對象纔是,因爲CallBackPtr的異常規範爲throw(),意味着此函數不會拋出任何異常。。而我在VS2008下卻能正常編譯通過,只有一個warning:C++ exception specification ignored except to indicate a function is not __declspec(nothrow),MSDN中對其解釋是:“使用異常規範聲明函數,Visual C++ 接受但並不實現此規範。包含在編譯期間被忽略的異常規範的代碼可能需要重新編譯和鏈接,以便在支持異常規範的未來版本中重用。”意即VC編譯器不支持異常規格說明。。 令我牽腸掛肚的是:倘若一直如此的話,那麼C++的異常規範特性得全部由程序員來掌控,感嘆VC編譯器對於這點多少有些不人道。。 但我也不想因此而以點蓋面的全盤否定VC編譯器在其它方面的高性能。

使用smart pointer來防止destructor中的資源泄露

對於由於異常處理不當而引發的資源泄露,無疑亦是程序員喜歡討論的話題之一,因爲拋出異常意味着一個拋異常的代碼塊可能只執行了一部分(前提是當前函數沒有處理異常),這樣的話,那麼異常又會傳送到當前代碼塊的外圍去處理,而引發資源泄露的代碼塊往往卻是這塊沒被執行的代碼,看看如下例子:

  1. #include <iostream> 
  2. using namespace std; 
  3.  
  4. class BaseClass 
  5. public
  6.     BaseClass(){}; 
  7.     ~BaseClass(){}; 
  8. }; 
  9.  
  10. void ExceptionFunc() throw(runtime_error) 
  11.     throw runtime_error("example exception handling!"); 
  12.  
  13. void Function() throw(runtime_error) 
  14.     BaseClass *pBase = new BaseClass; 
  15.  
  16.     ExceptionFunc(); 
  17.      
  18.     delete pBase; 
  19.  
  20. int main(int *argc , char **argv) 
  21.     try 
  22.     { 
  23.         Function(); 
  24.     } 
  25.     catch(runtime_error &err) 
  26.     { 
  27.         cout<<err.what()<<endl; 
  28.     } 
  29.     return 0; 

 

不得不承認在main函數返回之前,所有的異常確實得到了處理,讓人難以忽視的是:Function裏面拋出異常後,delete pBase沒有執行,這就意味着發生了內存泄露。。指針無處不在,如果我們不想用指針而想提高代碼的效率和質量,那幾乎是不可能的。事實上,我們可以在Function函數裏捕捉異常,然後在異常處理塊中執行delete pBase,可以避免由此引發的內存泄露,然而這樣做的缺陷是要寫兩個delete, Scott Meyers對於這種引發內存的更好的處理方式是:使用smart pointer。 如果將Function改爲如下:

  1. void Function() throw(runtime_error) 
  2.     BaseClass *pBase = new BaseClass; 
  3.     auto_ptr<BaseClass> PtrBase(pBase); 
  4.     ExceptionFunc(); 
 

用類來管理資源是防止資源泄露的有力法器之一,這種情況下異常拋出後,auto_ptr對象肯定會執行析構函數,此時會自動釋放其指針成員指向的對象資源,即便它的對象爲NULL,由於C++保證了delete空指針無異常的特性,所以資源是肯定會正確的釋放。然而在我看來,smart pointer的使用也只是一種折中而已,因爲使用auto_ptr而帶來的負面性後果其實也可以大作討論了,有待我之後的smart pointer文章再作詳細討論。  

異常逃離destructor的災難性後果

這一點也是唯一一條Sotte Meyyers在《effective c++》(條款8)和《more effective c++》(第五章節)中重複討論了兩次的條款,當destructor中無法處理異常的話,程序會直接調用teminate從而終止。。如果試圖在destructor外部捕獲異常,那將是徒勞的,正如一般重載delete運算符的聲明式一樣,往往在後面又加個異常規範throw(),這意味着delete外部根本無法捕捉到其內部的異常。看看下面這個簡單例子:

  1. #include <memory> 
  2. #include <iostream> 
  3. using namespace std; 
  4. class BaseClass 
  5. public
  6.     BaseClass(){}; 
  7.     ~BaseClass() 
  8.     { 
  9.         throw runtime_error("example runtime error."); 
  10.     }; 
  11. }; 
  12.  
  13. int main(int *argc , char **argv) 
  14.     BaseClass *pBase = new BaseClass; 
  15.     delete pBase; 
  16.     return 0; 

 

在VS2008下調用teminate時候還會調用abort,這個程序會非正常結束,如果在main函數中試圖這樣做:

  1. int main(int *argc , char **argv) 
  2.     BaseClass *pBase = new BaseClass; 
  3.     try 
  4.     { 
  5.         delete pBase; 
  6.     } 
  7.     catch(runtime_error &err) 
  8.     { 
  9.         cout<<err.what()<<endl; 
  10.     } 
  11.     return 0; 

 

結果會跟上面一樣(非正常結束),因爲delete是不會將任何異常傳遞到其外面的;一種比較折中的解決方法是,當destructor中存在異常拋出時,在destructor最後添加一個能捕獲所有異常的catch處理塊,catch處理塊又什麼工作都不做,如下:

 

  1. ~BaseClass() 
  2.     try 
  3.     { 
  4.         throw runtime_error("error in destructor"); 
  5.     } 
  6.     catch(...) 
  7.     { 
  8.     } 
  9. }; 

看起來是一種很壞很無奈的辦法,但正如Scott Meyers在《effective c++》中所說:

一般而言,將異常吞掉是個壞主意,因爲它壓制了"某些動作失敗"的重要信息!然而有時候吞下異常也比負擔"草率結束程序"或"不明確行爲帶來的風險好”。

 

 

後記

對於很多exception handling的概念性細節(比如何時使用引用類型的異常捕捉、異常捕獲層次的類型轉換等等)我沒做任何闡述,可以去看看《C++ PRIMER》的第十七章,有着很想盡的講解。。。 對於MS編譯器對異常規範的不支持,我很難理解,因爲G++編譯器確實是支持的。之前在討論C++的object佈局時(點擊這裏)也曾感嘆MS的編譯器在優化方面沒G++走得快,對於這些,或許是我運氣不好,老是碰到MS不如G++的地方,也或許是我現在幾乎不用G++編譯器的而體會不到其不如MS編譯器的地方的緣故吧。。。exception handling的確能爲提高代碼質量的改善作出或多說少的貢獻,但華麗麗的外表下,因爲用不好它而導致的程序的很多不明確(如teminate當前程序)和不正常(如資源泄露)行爲也是令人比較頭大的地方。貌似只有多熟用有技巧性的用是唯一能解決所有問題的方法了。。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章