C++異常處理的三個境界

2005年5月份,我轉正後1個月,組裏組織我們到青島旅遊,那個時候我正在看Exceptional C++這本書,有一個章節一直看不懂,就打印了帶到青島去了,嘿嘿,旅遊還是有助於激發靈感的,在旅館裏我終於看懂了,回來以後總結了一個PPT。這個PPT很有特點,因爲我做了一個Q版。時光飛逝阿,轉眼2年多過了,房價又漲了好多,本來可以買兩室兩廳的錢只夠一室一廳了。特重寫C++異常處理獻給我們可愛的房地產開發商.
 
Exception-Safety Issues and Techniques
預練武功,先修心法
看看金庸武俠小說中的那些少年俊才,在開始都是不一定有很高的技巧,但總是在年少的時候就或得到高人指點,或得到武林內功祕籍,並且都是心地善良。作爲一名C++的入門弟子,同樣,在碰到異常這個主題時,有兩條要銘記在心:
1. 異常安全不是口號,從一開始就要貫徹執行。
在設計程序時就要考慮的異常的處理,不要給自己藉口,說以後碰到了再說吧。千萬不能懶惰!等把代碼都寫的七七八八了,再來添加異常處理。
2. 要有懷疑一切的謹慎態度,要知道,所有的語句都是靠不住的。
如果你拿不出100%的信心說這段代碼一定exceptional-safe,那這段代碼就是異常不安全的。
總之,首先要培養 異常安全的意識。
兩個概念:
Exception-Safe
這裏的safe就是說從表現給外部的功能上來說,我總是正常工作了。至於我爲了正常工作,餓了肚子,取消了和女朋友的約會之類的事情,外部的調用代碼你們不需要關心。
Exception-Neutral:
我只是個傳話筒,我雖然是個基層領導,但是我不作爲,一線的員工有什麼意見,我一字不改的轉達給我的上司。下層函數拋上來的異常,我原封不動的throw給上層函數。但是並不是所有的基層領導都是不作爲的,他們要把底層員工的意思包裝一下,整理一下…然後再彙報給上層,這就叫做non-fully exception-neutral.
new和delete: 你們到底做了什麼?!
new和delete的兩步曲,我想大家都瞭解的:分配內存和調用構造函數/調用析構函數和釋放內存。本着懷疑一切的態度,如果分配內存失敗了呢,如果構造函數失敗了呢,如果析構函數失敗了呢。就new和delete的層次上,編譯器已經幫我們做了很多工作,如果我們希望new和delete是Exception-Safe的,最好打開編譯器的/GX開關。
我們來看一段小程序
template <class T>
Stack<T>::Stack()
:v_(0),     //這個Stack底層用數組實現,數組指針初始化爲0
vsize_(10), //希望爲數組分配可以容納10個元素的內存
vused_(0)   //現在這個Stack爲空
{
 v_ = new T[vsize_]; //initial allocation
}
分析一下這段代碼
1.這段代碼是exception-neutral的。如果new拋異常,Stack的構造函數只是原封不動的將下層異常throw給上層。
2.這段代碼沒有內存泄漏。如果內存分配了,但是某個T的構造函數失敗,/GX會保證new分配的內存會被釋放
3.這段代碼的狀態是合理的。如果內存分配失敗,std::bad_alloc被拋出,Stack的構造函數失敗,這是合理的。如果內存分配成功,第N(N<10)個T對象的構造函數失敗,/GX會保證前面N-1個已經被成功構造的對象的析構函數被調用,並且釋放內存。所以整體狀態也是合理的。
好了,正式進入修煉,本心法一共有3層
第一層: 我指保證沒有泄漏資源,其他的就難說了。
可以參考兩本書:《Coping with Exceptions》 和 《More Effective C++》
怎麼能保證我能夠到達第一層呢? 
第一層還是挺簡單的,只要不泄漏資源(內存)就可以了,一個常用的技巧是把一定要在函數退出時需要釋放的資源封裝在一個臨時對象中,這樣可以保證在函數退出時臨時對象析構時將資源釋放。
爲了保證第一層還有一句話是這樣講的:
惡魔!拋出異常的析構函數。爲什麼這樣說呢,並不是說析構函數不能拋出異常,原因是這樣的,如果析構函數是由於用戶代碼造成的,那不要緊。但是有時候析構函數是由/GX以後由編譯器添加的代碼調用的,看這樣的情況:
CMyObject* p = new CMyObject[10];
如果內存被成功分配,在調用第7個CMyObject的構造函數的時候有失敗,編譯器爲了保證行爲合理,會依次調用前面成功的6個對象的析構函數然後釋放內存,如果在這個時候前面6個析構函數中任何一個失敗(有異常拋出),就會調用Terminate終止程序,這是我們不願意看到的。爲了保證這一點,我們常常看到這樣的函數聲明:
void operator delete[](void*) throw();
void operator delete[](void*,size_t) throw();
 
第二層:我一切都很好,但還是受了內傷
我們看一段代碼,秉着每一條語句都是靠不住的意識來分析一下
template<class T>
void Stack<T>::Push(const T& t)
{
     if(vused_ == vsize_) //內存不夠,重新申請,等於判斷不會拋異常
     {
          size_t vsize_new = vsize_*2 + 1; //將內存擴大一倍,不會拋異常
          T* v_new         = NewCopy(v_,vsize_,vsize_new); //如果拋異常,內存沒有分配,該Stack對象的狀態不變(Safe)
          delete[] v_;     // this can’t throw(由第一層保證了)
          v_ = v_new;      // 奪取擁有權,賦值操作不拋異常
          vsize_ = vsize_new;  //賦值操作不拋異常
      }
      ++ vused_;         // 自加操作不拋異常
      v_[vused_] = t;   // 如果複製構造失敗,Stack狀態就不對了!
}
那不簡單,我把最後兩條語句改一下不就可以了嗎?!
v_[vused_+1] =t;
++vused_;
恩,沒有錯,從功能表現上來說,這樣就既滿足了exceptional-safe 又滿足了exceptionl-neutral。但是這個Stack還是受了內傷,她的內部狀態改變了哦!細心一點,我們來看一下:
假設現在這個stack中指向的內存空間的地址是0×1000(v_=0×1000),內存大小爲vsize_=10,已經用掉的內存大小vsize_=10,現在,在這個stack上進行一個push操作。好了,內存不夠,按照上面代碼的邏輯,假設到v_[vused_+1] =t;之前,所有的操作都成功,那現在stack中的狀態是這樣的:vsize_ =21, vused_=10,v_ =0×2000,現在v_[vused_+1] = t失敗,有異常拋出,好了,從外部看,一切都還是合理的。但是在stack內部,vsize_和v_都被改掉了,內存消耗增加了。這就是“內傷”,後面我會談到一個叫做shrink to fit的技巧來回收多餘的內存。
 
第三層:異常她輕輕的來了,又輕輕的走了…
這是異常處理的最高境界,善後工作做的事情好象從來沒有發生過一樣,這裏面一個基本邏輯就是commit-rollback。這裏推薦一本書<SGI Exception-Safe Standard Library Adaptation>,作者是 Dave Abrahams.
怎麼操作呢?
縱觀人類歷史,到處都有這樣的案例(就算是程序員,也要好好學歷史啊-:)):
找個替死鬼(Temp Object),賺了功勞都是我的(Swap),虧了過錯都是他的(Destroy by Exception-Unwinding)!
這裏有兩個技術細節,我們一個一個來欣賞:
1. 精心製作的copy constructor
王道–〉
Stack::Stack(const Stack&other):StackImpl<T>(other.vused_)
{
   while(vused_ < other.vused_)
   {
        construct(v_ + vused_, other.v_[vused_]);
        ++ vused_;
   }
}
2. 偷天換日的swap
template <class T>
void swap( T& a, T& b )
{
     T  temp(a); a = b; b = temp;
}
void StackImpl<T>::Swap( StackImpl & other)  throw()
 {
       swap( v_,     other.v_ );
       swap( vsize_, other.vsize_ );
       swap( vused_, other.vused_ );
 }
 
現在,push的代碼改成–>
void Push( const T& t )
 {
      if( vused_ == vsize_ )      // grow if necessary
      {
           Stack temp( vsize_*2+1 );
           while( temp.Count() < vused_ )
           {
                 temp.Push( v_[temp.Count()] );
           }
           temp.Push( t );
           Swap( temp );
      }
      else
      {
           construct( v_+ vused_, t );
           + + vused_;
      }
}
分析一下成功和失敗的情況下stack中的狀態變化.
 
<writing…>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章