【轉】剖析C++標準庫智能指針(std::auto_ptr)

轉自:http://www.cppblog.com/yuanyajie/archive/2006/12/15/16489.html

1.Do you Smart Pointer?

      Smart Pointer,中文名:智能指針, 舶來品?
      不可否認,資源泄露(resource leak)曾經是C++程序的一大噩夢.垃圾回收
      機制(Garbage Collection)一時頗受注目.然而垃圾自動回收機制並不能
      滿足內存管理的即時性和可視性,往往使高傲的程序設計者感到不自在.
      況且,C++實現沒有引入這種機制.在探索中,C++程序員創造了鋒利的
      "Smart Pointer".一定程度上,解決了資源泄露問題.

      也許,經常的,你會寫這樣的代碼:
      //x擬爲class:

//  class x{
      
//            public:        
      
//                   int m_Idata;
      
//            public:
      
//                   x(int m_PARAMin):m_Idata(m_PARAMin){}
      
//                   void print(){ cout<<m_Idata<<endl; }
      
//            .....
      
//            }
      
//
      void fook(){
      x
* m_PTRx = new A(m_PARAMin);
      m_PTRx
->DoSomething();     //#2
      delete m_PTRx;
      }


            是的,這裏可能沒什麼問題.可在複雜、N行、m_PTRclassobj所指對象生命周
      期要求較長的情況下,你能保證你不會忘記delete m_PTRclassobj嗎?生活中,
      我們往往不應該有太多的口頭保證,我們需要做些真正有用的東西.還有一個
      更敏感的問題:異常.假如在#2方法執行期異常發生,函數執行終止,那麼new
      出的對象就會泄露.於是,你可能會說:那麼就捕獲異常來保證安全性好了.
      你寫這樣的程式:

    void fook(){
      A
* m_PTRx = new x(m_PARAMin);
      
try{
          m_PTRx
->DoSomething();
      }

      
catch(..){
          delete m_PTRx;
          
throw;
      }

      delete m_PTRx;
      }

      哦!天哪!想象一下,你的系統,是否會象專爲捕獲異常而設計的.

      一天,有人給你建議:"用Smart Pointer,那很安全.".你可以這樣重寫你的程序:
   

      void fook(){
      auto_ptr
<x> m_SMPTRx(new x(m_PARAMin));
      m_SMPTRx
->DoSomething();
      }

 

      OK!你不太相信.不用delete嗎?
      是的.不用整天提心吊膽的問自己:"我全部delete了嗎?",而且比你的delete
      策略更安全.

      然後,還有人告訴你,可以這樣用呢:

 

 ok1.
      auto_ptr
<x> m_SMPTR1(new x(m_PARAMin)); 
      auto_ptr
<x> m_SMPTR2(m_SMPTR1);  //#2
      May be you can code #2 like this :
          auto_ptr
<x> m_SMPTR2;
          m_SMPTR2 
= m_SMPTR1;      
      ok2.
      auto_ptr
<int> m_SMPTR1(new int(32));
      
      ok3.
      auto_ptr
<int> m_SMPTR1;
      m_SMPTR1 
= auto_ptr<int>(new int(100));
      也可以:
      auto_ptr
<int> m_SMPTR1(auto_ptr<int>(new int(100)));
      
      ok4.
      auto_ptr
<x> m_SMPTR1(new x(m_PARAMin));
      m_SMPTR1.reset(
new x(m_PARAMin1));
      
      ok5.
      auto_ptr
<x> m_SMPTR1(new x(m_PARAMin));
      auto_ptr
<x> m_SMPTR2(m_SMPTR.release());
      cout
<<(*m_SMPTR2).m_Idata<<endl;  
      
      ok6.
      auto_ptr
<int> fook(){
      
return auto<int>(new int(100));
      }

 


           ok7.............and so on
     
      但不可這樣用:
      
      

no1.   
      
char* chrarray = new char[100];
      strcpy(chrarray,
"I am programming.");
      auto_ptr
<char*> m_SMPTRchrptr(chrarray);
      
//auto_ptr並不可幫你管理數組資源     
       
      no2.
      vector
<auto_ptr<x>> m_VECsmptr;
      m_VECsmptr.push_back(auto_ptr
<int>(new int(100)));
      
//auto_ptr並不適合STL內容.
       
      no3.
      
const auto_ptr<x> m_SMPTR1(new x(100));
      auto_ptr
<x> m_SMPTR(new x(200));
      
      no4.
      x m_OBJx(
300);
      auto_ptr
<x> m_SMPTR(&m_OBJx);
      
      no5
      x
* m_PTR = new x(100);
      auto_ptr
<x> m_SMPTR = m_pTR;
      


      no6..........and so on

      預先提及所有權的問題,以便下面帶着疑問剖析代碼?

 

      power1.
      auto_ptr
<x> m_SMPTR1(new x(100));
      auto_ptr
<x> m_SMPTR2 = m_SMPTR1;
      m_SMPTR2
->print();
      
//輸出:100.
      m_SMPTR1->print();
      
//!! 非法的.

      power2.
      auto_ptr
<x> m_SMPTR(new x(100));
      
      auto_ptr
<x> returnfun(auto_ptr<x> m_SMPTRin){
      
return m_SMPTRin;
      }

      
      auto_ptr
<x> = returnfun(m_SMPTR);  //#5

 

      //在上面的#5中,我要告訴你對象所有權轉移了兩次.
      //什麼叫對象所有權呢?
  
    2. std::auto_ptr的設計原理
      
      上面的一片正確用法,它們在幹些什麼?
            一片非法,它們犯了什麼罪?
            一片什麼所有權轉移,它的內部機智是什麼?
      哦!一頭霧水?下面我們就來剖析其實現機制.
      基礎知識:
              a.智能指針的關鍵技術:在於構造棧上對象的生命期控制
                堆上構造的對象的生命期.因爲在智能指針的內部,存儲
                着堆對象的指針,而且在構析函數中調用delete行爲.
                大致機構如下:
                x* m_PTRx = new x(100);//#1
                template<typename T>
                auto_ptr{
                private:
                T* m_PTR;//維護指向堆對象的指針,在auto_ptr定位後    
                ....     //它應該指向#1構造的對象,即擁有所有權.
                ~auto(){ delete m_PTR; }
                ....
                }
             b.所有權轉移之說
               上面曾有一非法的程式片段如下:
               auto_ptr<x> m_SMPTR1(new x(100));
               auto_ptr<x> m_SMPTR2 = m_SMPTR1;
               m_SMPTR2->print();
               //輸出:100.
               m_SMPTR1->print();
               //!! 非法的.
               按常理來說,m_SMPTR->print();怎麼是非法的呢?
               那是因爲本來,m_SMPTR1維護指向new x(100)的指針,
               可是m_SMPTR2 = m_SMPTR1;auto_ptr內部機制使得m_SMPTR1將對象的地址
               傳給m_SMPTR2,而將自己的對象指針置爲0.
               那麼自然m_SMPTR->print();失敗.
               這裏程序設計者要負明顯的職責的.
               那麼auto_ptr爲什麼採取這樣的策略:保證所有權的單一性.
                                               亦保證了系統安全性.
               如果多個有全權的auto_ptr維護一個對象,那麼在你消除一個
               auto_ptr時,將導致多個auto_ptr的潛在危險.
     
       下面我們以SGI-STL的auto_ptr設計爲樣本(去掉了無關分析的宏),來剖析其原理.

 

#1  template <class _Tp> class auto_ptr {
       #
2  private:
       #
3  _Tp* _M_ptr;  //定義將維護堆對象的指針

       #
4  public:
       #
5  typedef _Tp element_type;  //相關類型定義
       #6  explicit auto_ptr(_Tp* __p = 0) __STL_NOTHROW : _M_ptr(__p) {}
       #
7  auto_ptr(auto_ptr& __a) __STL_NOTHROW : _M_ptr(__a.release()) {}
       #
8  template <class _Tp1> auto_ptr(auto_ptr<_Tp1>& __a) __STL_NOTHROW
                                                 : _M_ptr(__a.release()) 
{}
           
//#6、#7、#8是auto_ptr構造函數的三個版本.
           
//#6註釋:傳入對象的指針,構造auto_ptr.explicit關鍵字:禁止隱式轉換.
           
//        這就是ok2正確,而no5(隱式轉換)錯誤的原因.
           
//#7註釋:拷貝構造函數.
           
//        傳入auto_ptr實例,構造auto_ptr. ok1、ok3使用了這個構造式.
           
//        它是一個很關鍵的構造函數,在具體情況下,我們再分析
           
//#8註釋:auto_ptr的模板成員,可在繼承對象重載的基礎上,實現特殊功能.
           
//   
           
//   舉例:
           
//   class A{ public: 
           
//          virtual void fook(){cout<<"I am programming"<<endl;
           
//          /*..........*/                                   }; 
           
//   class B : public A {
           
//          virtual void fook(){ cout<<"I am working"<<endl;
           
//         /*...........*/                                  };  
           
//   auto_ptr<A> m_SMPTRa(new A(33));//實質:
           
//   auto_ptr<B> m_SMPTRb(m_SMPTRa); //基類的指針可以賦給派生類的指針          
           
//              
           
//   auto_ptr<B> m_SMPTRb(new B(44));//實質:
           
//   auto_ptr<A> m_SMPTRa(m_SMPTRb); //派生類的指針不可賦給基類的指針
           
//       
           
//   auto_ptr<A> m_SMPTRa(new B(33));  // ok!  
           
//   m_SMPTRa->fook()將調用派生類B的fook()
           
//   m_SMPTRa->A::fook()將調用基類A的fook()
           
//    
           
//   auto_ptr<B> m_SMPTRb(new A(33));  // wrong!
           
//   
           
//   
       #9  auto_ptr& operator=(auto_ptr& __a) __STL_NOTHROW {
       #
10 if (&__a != this{ delete _M_ptr;  _M_ptr = __a.release(); }
       #
11 return *this;
       #
12 }

         
       #
13 template <class _Tp1>
       #
14 auto_ptr& operator=(auto_ptr<_Tp1>& __a) __STL_NOTHROW {
       #
15 if (__a.get() != this->get()) { delete _M_ptr; _M_ptr = __a.release(); }
       #
16 return *this;
       #
16 }
  
          
//
          
// #9~~#16 兩個版本的指派函數.
          
//         delete _M_ptr; 在指派前,銷燬原維護的對象.
          
//         _a.release() ; release操作,詳細代碼參見#20~~#23.
          
//                        用於*this獲得被指派對象,
          
//                        且將原維護auto_ptr置空.
          
//     no3使用了第一種指派.
          
//     而權限轉移正是_a.release()的結果.
          
       #
17 ~auto_ptr() __STL_NOTHROW { delete _M_ptr; }
          
//構析函數.消除對象.注意這裏對對象的要求!
          
       #
17 _Tp& operator*() const __STL_NOTHROW {  return *_M_ptr; }
       #
18 _Tp* operator->() const __STL_NOTHROW return _M_ptr;  }
       #
19 _Tp* get() const __STL_NOTHROW return _M_ptr; }
         
//
         
//  操作符重載.
         
// #17註釋:提領操作(dereference),獲得對象. 見ok5用法.
         
// #18註釋:成員運算符重載,返回對象指針.
         
// #19註釋:普通成員函數.作用同於重載->運算符
         
//
       #20 _Tp* release() __STL_NOTHROW {
       #
21 _Tp* __tmp = _M_ptr;
       #
22 _M_ptr = 0;
       #
23 return __tmp;                }

         
//上面已經詳解      
 
       #
24 void reset(_Tp* __p = 0) __STL_NOTHROW {
       #
25 delete _M_ptr;
       #
26 _M_ptr = __p;                          }

         
//
         
//傳入對象指針,改變auto_ptr維護的對象
         
//       且迫使auto_ptr消除原來維護的對象
         
//       見ok3用法.

         
// According to the C++ standard, these conversions are required.  Most
         
// present-day compilers, however, do not enforce that requirement---and, 
         
// in fact, most present-day compilers do not support the language 
         
// features that these conversions rely on.
         


                //下面這片段用於類型轉化,目前沒有任何編譯器支持
         //具體技術細節不訴.         

 

         

#ifdef __SGI_STL_USE_AUTO_PTR_CONVERSIONS

      #
27 private:
      #
28 template<class _Tp1> 
      #
29 struct auto_ptr_ref { _Tp1* _M_ptr; auto_ptr_ref(_Tp1* __p) : _M_ptr(__p) {}
                             }
;

      #
30 public:
      #
31 auto_ptr(auto_ptr_ref<_Tp> __ref) __STL_NOTHROW
                               : _M_ptr(__ref._M_ptr) 
{}
      #
32 template <class _Tp1> 
      #
33 operator auto_ptr_ref<_Tp1>() __STL_NOTHROW 
      #
34 return auto_ptr_ref<_Tp>(this->release()); }
      #
35 template <class _Tp1> operator auto_ptr<_Tp1>() __STL_NOTHROW
      #
36 return auto_ptr<_Tp1>(this->release()); }
      #
37 #endif /* __SGI_STL_USE_AUTO_PTR_CONVERSIONS */
      #
38 };

      
      OK!就是這樣了.
      正如上面原理介紹處敘說,
      你需要正視兩大特性:
      1.構造棧對象的生命期控制堆上構造的對象的生命期
      2.通過release來保證auto_ptr對對象的獨權.
     
     在我們對源碼分析的基礎上,重點看看:
     no系列錯誤在何處?
     no1.
         我們看到構析函數template<class _Tp>
                         ~auto_ptr() _STL_NOTHROW
                        { delete _M_ptr; }
         所以它不能維護數組,
         維護數組需要操作:delete[] _M_ptr;
     no2.
        先提部分vector和auto_ptr代碼:
        a.提auto_ptr代碼
          
        

auto_ptr(auto_ptr& __a) __STL_NOTHROW : _M_ptr(__a.release()) {}

        
        b.提vector代碼
         


          Part1:
          
void push_back(const _Tp& __x) {
          
if (_M_finish != _M_end_of_storage) {
          construct(_M_finish, __x);
          
++_M_finish;
          }

          
else
         _M_insert_aux(end(), __x);
          }

        
         Part2:
         template 
<class _T1, class _T2>
         inline 
void construct(_T1* __p,

         
//++++++++++++++++++++++++++++++++ 
         
//         const _T2& __value) { +
         
//++++++++++++++++++++++++++++++++
         
//  new (__p) _T1(__value);      +
         
//++++++++++++++++++++++++++++++++

         }
         
         Part3.
         template 
<class _Tp, class _Alloc>
         
void 
         vector
<_Tp, _Alloc>::_M_insert_aux
         (iterator __position,

          
//++++++++++++++++++++++++++++++++ 
          
//        const _Tp& __x)       ++
          
//++++++++++++++++++++++++++++++++   
 
         
{
         
if (_M_finish != _M_end_of_storage) {
         construct(_M_finish, 
*(_M_finish - 1));
         
++_M_finish;

         
//++++++++++++++++++++++++++++++++
         
//     _Tp __x_copy = __x;       +
         
//++++++++++++++++++++++++++++++++

         copy_backward(__position, _M_finish 
- 2, _M_finish - 1);
         
*__position = __x_copy;
         }

         
else {
         
const size_type __old_size = size();
         
const size_type __len = __old_size != 0 ? 2 * __old_size : 1;
         iterator __new_start 
= _M_allocate(__len);
         iterator __new_finish 
= __new_start;
         __STL_TRY 
{
         __new_finish 
= uninitialized_copy
         (_M_start, __position, __new_start);
         construct(__new_finish, __x);
         
++__new_finish;
         __new_finish 
= uninitialized_copy
        (__position, _M_finish, __new_finish);
        }

        __STL_UNWIND((destroy(__new_start,__new_finish), 
                  _M_deallocate(__new_start,__len)));
       destroy(begin(), end());
       _M_deallocate(_M_start, _M_end_of_storage 
- _M_start);
       _M_start 
= __new_start;
       _M_finish 
= __new_finish;
       _M_end_of_storage 
= __new_start + __len;
       }

       }

 

       從提取的vector代碼,Part1可看出,push_back的操作行爲.
       兵分兩路,可是再向下看,你會發現,無一例外,都
       通過const _Tp& 進行拷貝行爲,那麼從auto_ptr提出的片段就
       派上用場了.
       可你知道的,auto_ptr總是堅持對對象的獨權.那必須修改
       原來維護的對象,而vector行爲要求const _Tp&,這樣自然會產生
       問題.一般編譯器是可以發覺這種錯誤的.

       其實,STL所有的容器類都採用const _Tp&策略.
 
       //+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
      + 看了sutter和Josuttis的兩篇文章中,都提及:                    +
      + STL容器不支持auto_ptr原因在於copy的對象只是獲得所有權的對象, +
      + 這種對象不符合STL的要求.可是本人總感覺即時不是真正的複製對象,+
      + 但我用vector<auto_ptr<x> >的目的就在於維護對象,並不在乎      +
      + 所謂的完全對象.而且我用自己寫的Smart Pointer配合STL容器工作, +
      + 很正常.那需要注意的僅僅是const問題.                          +
      +                                                              +
      //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

     no3.
        這個也是auto_ptr隱含的所有權問題引起的.
        const auto_ptr不允許修改.
        隨便提及:const對象不代表對象一點不可以改變.
                  在兩種const語義下,都有方法修改對象或對象內部指針維護的對象
                 或其它資源.
     no4.
        再看auto_ptr的構析函數.
        delete不可以消除棧上資源.

     no5.
        依賴傳入對象指針的構造函數被聲明爲explicit,禁止隱式轉換.

   
    3.auto_ptr高級使用指南
     
      a.類成員auto_ptr,禁止構造函數以構建"完全對象"

 

Programme1:
        
struct Structx{
               
int m_Idata;
               
char m_CHRdata;
               
/* and so on */
        }
;
        出於對象編程的理念,
        我們將Structx打造成包裹類:
        
class StructWrapper{
        
private:
        Structx
* m_STRTxptr;
        
public:
        StructWrapper():m_STRTxptr(
new Structx){}
        
~StructWrapper(){delete m_SMRTxptr; }
        
public:
        
void Soperator1()/* 針對Structx對象的特性操作 */}
        
void Soperator2()/* 針對Structx對象的特性操作 */}        
        
/*  and so on */
        }

        
        Programme2:
        
class StructWrapper{
        
private:
        auto_ptr
<Structx> m_SMPTRx;
        
public:
        StructWrapper():m_SMPTRAx(
new Structx){}
        
public:
        
void Soperator1()/* 針對Structx對象的特性操作 */}
        
void Soperator2()/* 針對Structx對象的特性操作 */}        
        
/*  and so on */
        }

        
        Programme3:
        StructWrapper::StructWrapper(
const StructWrapper& other)
        : M_SMPTRx(
new Struct(*other.m_SMPTRx)) { }
        StructWrapper
& StructWrapper::operator=(const StructWrapper &other){
        
*m_SMPTRx = *other.m_SMPTRx;
        }
;


                處於對構建於堆中的對象(new Structx)智能維護的需要.
        我們將programme1改造爲programme2:
        不錯,對象是可以智能維護了.
        對於包裹類(StructWrapper)你是否會有這樣的構造或指派操作:
         StructWrapper m_SMPTRWrapper2(m_SMPTRWrapper1);
      
         StructWrapper mSMPTRWrapper2 = m_SMPTRWrapper1;
         那麼請注意:
         當你坦然的來一個:M_SMPTRWrapper1->Soperator1();的時候,
         系統崩潰了.
         不必驚訝,所有權還是所有權問題.
         問一下自己:當programme2默認拷貝構造函數作用時,又調用了auto_ptr的
         默認構造函數,那麼auto_ptr所有的默認行爲都遵循獨權策略.對,就這樣.
         m_SMPTRWrapper1的對象所有權轉移給了m_SMPTRWrapper2.
         M_SMPTRWrapper1->Soperator1();那麼操作變成了在NULL上的.
         哦!系統不崩潰纔怪.
         那麼你需要想,programme3那樣利用auto_ptr的提領操作符自己的
         構造"完全對象".

       b.利用const關鍵字,防止不經意的權限轉移
        
         從上面的敘述,你可看出,所有權轉移到處可以釀成大禍.
         而對於一般應用來說,獨權又是很好的安全性策略.
         那麼我們就用const來修飾auto_ptr,禁止不經意的錯誤.
       
         當然上面提及:並不代表auto_ptr是不可修改的.
         處於需要,從兩種const語義,你都可實現修改.

         然,你還希望在函數傳入傳出auto_ptr那麼你可傳遞auto_ptr的引用,
         那就萬無一失了: void fook(const auto_ptr<x>& m_PARAMin);
         在返回後賦予其它時,使用引用是不行的.你得用指針.
         因爲引用無論作爲lvalue還是rvaluev,都會調用構造或指派函數.


    4.你是否覺得std::auto_ptr還不夠完美
     
      在實踐中,std::auto_ptr能滿足你的需求嗎?          
 
      Andrei Alexandrescu在一篇文章中,提及:有關Smart Pointer的技術就像
      巫術.Smart Pointer作爲C++垃圾回收機制的核心,它必須足夠強大的、具有工業強度和安全性.
      但爲了可一勞永逸我們還需要披荊斬棘繼續探索.

      下面在需求層面上,我們思索一下我們的智能指針還需要些什麼?
 
        a. std::auto_ptr 能夠處理數組嗎?我們可以用智能指針來管理其它的資源嗎?
           譬如一個線程句柄、一個文件句柄 and so on !
        b. 對於我們的對象真的永遠實行獨權政策嗎?
        c. Our 智能指針還需要在繼承和虛擬層面上發揮威力 !
        d. 往往,需要擴展Our 智能指針的功能成員函數來滿足動態的需要 !
        e. 也許,你需要的還很多.

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