前言
前幾天在很多地方老是碰到RAIIResouce Acqusition Is Initialition)相關的話題,對於這一塊,由於自己以前在代碼中很少用到,從來都習慣於使用dumb pointer,所以從沒仔細去研究過。當它足夠頻繁的出現在我的眼前時,我漸漸意識到,是時候該做個了斷了說“了斷”貌似有些誇張,其實也只是想把它研究透,以免以後老出現在我的眼前而不知其內部原理。。)。事實上,我當早該寫這篇博文了,只是當我在看標准庫的auto_ptr源碼時,又發現裡面的exception handling聲明很多,困惑的地方總有該了結的時候,情急之下,又去鑽透了exception handling可以看看我之前的一篇博文:C++華麗的exception handling(異常處理)背後隱藏的陰暗面及其處理方法)。
在諸多大師書籍中,關於smart pointer的話題,《effective c++》中在討論resource management時有涉及到,但僅僅是簡單的一點用法,實質性原理方面沒涉及到;《c++ primer》同樣很少;《the c++ standard library》中倒是對auto_ptr講解的很透徹,對於同樣在TR1標准庫中存在的shard_ptr卻一筆帶過,究其原因是由於作者在寫書時,tr1庫還未納入C++標准;而Scott Meyers在《more effective c++》中對於smart pointer的原理性剖析非常詳細,以至於像是在教我們如何設計一個良好的auto_ptr class和shard_ptr class。。 事實上,當我在不清楚smart pointer原理的時候,我開始在想:以後的代碼中一定要用smart pointer代替dumb pointer,但當我真正了解了其內部機制後,卻多少有些膽怯,因為相對於smart pointer所帶來的方便性而言,由於使用其而帶來的負面後果著實讓人望而生畏。。
auto_ptr並非一個四海通用的指針
對於auto_ptr在解決exception handling時內存管理方面所作出的貢獻是值得肯定的,這方面我不再想闡述具體內容,可以看看這裡,在此我只想討論起所帶來的負面性後果。總結起來,值得注意的地方有以下幾點:
1.auto_ptrs不能共享擁有權;
2.並不存在針對array而設計的auto_ptr;
3.auto_ptr不滿足STL容器對其元素的要求;
4.派生類dumb pointer所對應的auto_ptr對象不能轉換為基類dumb pointer所對應的auto_ptr對象;
我將逐個詳細闡述說明這4點,為此,先來看標准庫中auto_ptr的一段源碼:
- template<class _Ty>
- class auto_ptr
- { // wrap an object pointer to ensure destruction
- public:
- typedef _Ty element_type;
- explicit auto_ptr(_Ty *_Ptr = 0) _THROW0()
- : _Myptr(_Ptr)
- { // construct from object pointer
- }
- auto_ptr(auto_ptr<_Ty>& _Right) _THROW0()
- : _Myptr(_Right.release())
- { // construct by assuming pointer from _Right auto_ptr
- }
- auto_ptr(auto_ptr_ref<_Ty> _Right) _THROW0()
- { // construct by assuming pointer from _Right auto_ptr_ref
- _Ty *_Ptr = _Right._Ref;
- _Right._Ref = 0; // release old
- _Myptr = _Ptr; // reset this
- }
- template<class _Other>
- operator auto_ptr<_Other>() _THROW0()
- { // convert to compatible auto_ptr
- return (auto_ptr<_Other>(*this));
- }
- template<class _Other>
- operator auto_ptr_ref<_Other>() _THROW0()
- { // convert to compatible auto_ptr_ref
- _Other *_Cvtptr = _Myptr; // test implicit conversion
- auto_ptr_ref<_Other> _Ans(_Cvtptr);
- _Myptr = 0; // pass ownership to auto_ptr_ref
- return (_Ans);
- }
- template<class _Other>
- auto_ptr<_Ty>& operator=(auto_ptr<_Other>& _Right) _THROW0()
- { // assign compatible _Right (assume pointer)
- reset(_Right.release());
- return (*this);
- }
- template<class _Other>
- auto_ptr(auto_ptr<_Other>& _Right) _THROW0()
- : _Myptr(_Right.release())
- { // construct by assuming pointer from _Right
- }
- auto_ptr<_Ty>& operator=(auto_ptr<_Ty>& _Right) _THROW0()
- { // assign compatible _Right (assume pointer)
- reset(_Right.release());
- return (*this);
- }
- auto_ptr<_Ty>& operator=(auto_ptr_ref<_Ty> _Right) _THROW0()
- { // assign compatible _Right._Ref (assume pointer)
- _Ty *_Ptr = _Right._Ref;
- _Right._Ref = 0; // release old
- reset(_Ptr); // set new
- return (*this);
- }
- ~auto_ptr()
- { // destroy the object
- delete _Myptr;
- }
- _Ty& operator*() const _THROW0()
- { // return designated value
- #if _HAS_ITERATOR_DEBUGGING
- if (_Myptr == 0)
- _DEBUG_ERROR("auto_ptr not dereferencable");
- #endif /* _HAS_ITERATOR_DEBUGGING */
- __analysis_assume(_Myptr);
- return (*get());
- }
- _Ty *operator->() const _THROW0()
- { // return pointer to class object
- #if _HAS_ITERATOR_DEBUGGING
- if (_Myptr == 0)
- _DEBUG_ERROR("auto_ptr not dereferencable");
- #endif /* _HAS_ITERATOR_DEBUGGING */
- return (get());
- }
- _Ty *get() const _THROW0()
- { // return wrapped pointer
- return (_Myptr);
- }
- _Ty *release() _THROW0()
- { // return wrapped pointer and give up ownership
- _Ty *_Tmp = _Myptr;
- _Myptr = 0;
- return (_Tmp);
- }
- void reset(_Ty* _Ptr = 0)
- { // destroy designated object and store new pointer
- if (_Ptr != _Myptr)
- delete _Myptr;
- _Myptr = _Ptr;
- }
- private:
- _Ty *_Myptr; // the wrapped object pointer
- };
- _STD_END
對於第一點,容易犯的一個錯誤是很多時候試圖將同一個dumb pointer賦給多個auto_ptr,不管是不知道auto_ptr的用法而導致或是因為忘記了是否已經將一個dumb pointer之前移交給了一個auto_ptr管理,結果將是災難性的盡管編譯能通過)。比如這樣:
- class BaseClass{};
- int test()
- {
- BaseClass *pBase = new BaseClass;
- auto_ptr<BaseClass> ptrBaseClass1(pBase);
- auto_ptr<BaseClass> ptrBaseClass2(pBase);
- return 0;
- }
auto_ptr源碼中看出來對於對象的_Mypt在constructor中進行了初始化,而在destructor中對_Mypt又進行了delete,這意味著在上述代碼中,test函數執行完時對同一個pBase連續delete了兩次。在WIN32下會出現assert然後終止運行。如果想讓多個RAII對象共享同一個dumb pointer,卻依然不想考慮由誰來釋放pointer的內存,那麼在通盤考慮合適的情況下可以去用shard_ptr後面會詳細講解)。
另外一個比較容易犯的錯誤是:試圖將auto_ptr以by value或by reference方式傳遞給一個函數形參,其結果同樣是災難性的,因為這樣做這意味著所有權進行了移交,比如試圖這樣做:
- void test(auto_ptr<int> ptrValue1)
- {
- if (ptrValue1.get())
- {
- cout<<*ptrValue1<<endl;
- }
- }
- int main()
- {
- auto_ptr<int> ptrValue(new int);
- *ptrValue = 100;
- test(ptrValue);
- *ptrValue = 10;
- cout<<*ptrValue<<endl;
- return 0;
- }
如果用習慣了dumb pointer,或許會以為這樣做沒有任何錯誤,但實際結果卻是跟上述例子一樣:出現assert然後teminate了當前程序。test之後ptrValue已經成為NULL值,對一個NULL進行引用並賦值,結果是未定義的。。倘若以by reference方式替代by value方式呢?那麼將test改為如下:
- void test(auto_ptr<int> &ptrValue1)
- {
- if (ptrValue1.get())
- {
- cout<<*ptrValue1<<endl;
- }
- }
如此以來,所得出的結果也正是我們所期望的,看起來貌似不錯,但倘若有人這樣做:
- void test(auto_ptr<int> &ptrValue1)
- {
- auto_ptr<int> ptrValue2 = ptrValue1;
- if (ptrValue2.get())
- {
- cout<<*ptrValue2<<endl;
- }
- }
結果會和之前的by value方式一樣,同樣是災難性的。如果非要讓auto_ptr通過參數傳遞進一個函數中,而且不影響其後續時候,那麼只有一種方式:by const reference。如此的話,如果試圖這樣做:
- void test(const auto_ptr<int> &ptrValue1)
- {
- auto_ptr<int> ptrValue2 = ptrValue1;
- if (ptrValue2.get())
- {
- cout<<*ptrValue2<<endl;
- }
- }
將不會通過編譯,因為ptrValue2 = ptrValue1試圖在改變const reference的值。
對於第二點,從源碼中看出來,destructor只執行delete _Mypt,而不是delete []_Mypt;所以如果試圖這樣做:
- class BaseClass{};
- int test()
- {
- BaseClass *pBase = new BaseClass[5];
- auto_ptr<BaseClass> ptrBaseClass1(pBase);
- return 0;
- }
注意如此會發生內存洩露,pBase實際指向的是數組首元素,這意味著只有pBase[0]被正常釋放了,其它對象均沒被釋放。標准庫中至今不存在一個可以管理動態分配數組的auto_ptr或shared_ptr,這方面,boost::scoped_array和boost::shared_array可以提供這樣的功能,或許以後在適當的時候我會再次深入講解這兩個RAII class。
對於第三點,auto_ptr在=操作符和copy constructor中的行為可從源碼中看出來,其實質是進行了_Mypt管理權限的交接,這也正是auto_ptr一開始奉行的遵旨:只讓一個RAII class object來管理同一個dumb pointer,若非如此,那麼auto_ptr的存在是毫無意義的。而STL容器對其元素的值語意的要求是:可拷貝構造意味著其元素與被拷貝元素的值相同。事實上,諸如vector等容器經常push_back,pop_back或之類的操作會返回一個副本或拷貝一個副本,所以要求其值語意為拷貝後與原元素值還要保持相同就理所當然了。auto_ptr進行拷貝後,元素值就會發生改變,如此即不符合STL的值語意要求。
對於第四點而言,dumb poiter的派生類可以自由的轉換為其所對應的基類的dumb pointer,而auto_ptr卻不能,因為auto_ptr是個單獨的類,意味著任何兩個auto_ptr對象不能像普通指針那樣進行這類轉換,比如這樣做:
- class BaseClass{};
- class DerivedClass{};
- int test()
- {
- auto_ptr<BaseClass> ptrBaseClass;
- auto_ptr<DerivedClass> ptrDerivedClass(new DerivedClass);
- ptrBaseClass = ptrDerivedClass;
- return 0;
- }
是個錯誤的做法,這段代碼將不會通過編譯;事實上,這也正是所有現行RAII class存在的瓶頸,除非自己去設計一個RAII class,可以自由定義隱式轉換操作符,比如這樣做:
- template<typename T>
- class SmartPointBaseClass
- {
- private:
- T* ptr;
- public:
- SmartPointBaseClass(T* point = NULL):ptr(point){}
- ~SmartPointBaseClass()
- {
- delete ptr;
- }
- };
- template<typename T>
- class SmartPointDerivedClass:public SmartPointBaseClass<T>
- {
- private:
- T* ptr;
- public:
- SmartPointDerivedClass(T* point = NULL):ptr(point)
- {}
- operator SmartPointBaseClass()
- {
- SmartPointBaseClass basePtr(ptr);
- ptr = NULL;
- return basePtr;
- };
- ~SmartPointDerivedClass()
- {
- delete ptr;
- }
- };
- int test()
- {
- SmartPointBaseClass<int> ptrBaseClass;
- SmartPointDerivedClass<int> ptrDerivedClass(new int);
- ptrBaseClass = ptrDerivedClass;
- return 0;
- }
這裡我重載了SmartPointBaseClass的隱式轉換操作符,從而得以讓派生類auto_ptr可以隱式轉換為基類的auto_ptr。這是一個很簡陋的RAII class,簡陋到我自己都不敢用了^_^。。其實只是用來說明原理而用這段代碼在VS下測試通過),倘若真想設計一個良好的通用性強的RAII class,個人認為要仔細看看《more effective c++》中的條款28和29了,另外還得考慮到結合自身需求制定出性能和功能都比較折中或更良好的auto_ptr。All in all,auto_ptr絕對不是一個四海通用的指針。
auto_ptr的替代方案——shared_ptr
對於shared_ptr,其在很多方面能解決auto_ptr的草率行為如以by value或by reference形式傳遞形參的災難性後果)和限制性行為如當做容器元素和多個RAII object共同擁有一個dumb pointer主權),它通過reference counting來使得多個對象同時擁有一個主權,當所有對象都不在使用其時,它就自動釋放自己。如此看來,Scott Meyers稱其為一個垃圾回收體系其實一點也不為過。由於TR1中的shared_ptr代碼比較多,而且理解起來很困難。那麼看看下面代碼,這是《the c++ stantard library》中的一個簡易的reference counting class源碼,其用來說明shared_ptr原理來用是足夠了的:
- template<typename T>
- class CountedPtr
- {
- private:
- T* ptr;
- long *count;
- public:
- explicit CountedPtr(T* p = NULL):ptr(p),count(new long(1))
- {}
- CountedPtr(const CountedPtr<T> &p)throw():ptr(p.ptr),count(p.count)
- {
- ++*count;
- }
- ~CountedPtr()throw()
- {
- dispose();
- }
- CountedPtr<T>& operator = (const CountedPtr<T> &p)throw()
- {
- if (this != &p)
- {
- dispose();
- ptr = p.ptr;
- count = p.count;
- ++*count;
- }
- return *this;
- }
- T& operator*() const throw()
- {
- return *ptr;
- }
- T* operator->()const throw()
- {
- return ptr;
- }
- private:
- void dispose()
- {
- if (--*count == 0)
- {
- delete count;
- delete ptr;
- }
- }
- };
TR1的shared_ptr比這復雜很多,它的counting機制由一個專門的類來處理,因為它還得保證在多線程環境中counting的線程安全性;另外對於對象的析構工作具體處理形式,其提供了一個函數對象來供用戶程序員來自己控制,在構造時可以通過參數傳遞進去。
shared_ptr在構造時,引用計數初始化為1,當進行復制控制時,對於shared_ptr先前控制的資源進行引用計數減1為0時銷毀先前控制的資源),因為此時當前shared_ptr要控制另外一個dumb pointer,所以其又對新控制的shared_ptr引用計數加1。
這樣好了,由於其支持正常的賦值操作,所以能做容器的元素使用,也因此可以放心的用來進行函數形參的傳遞而不用擔心像auto_ptr那樣的權利轉交所帶來的災難性後果了。但事實不盡如此,auto_ptr的權利轉交所帶來的便利性就是:永遠不會存在循環引用的對象而導致內存洩露,而shared_ptr卻開始存在這樣的問題了,比如下面代碼:
- class BaseClass;
- class DerivedClass;
- class BaseClass
- {
- public:
- tr1::shared_ptr<DerivedClass> sptrDerivedClass;
- };
- class DerivedClass
- {
- public:
- tr1::shared_ptr<BaseClass> sptrBaseClass;
- };
- void InitData()
- {
- tr1::shared_ptr<BaseClass> baseClass(new BaseClass);
- tr1::shared_ptr<DerivedClass> derivedClass(new DerivedClass);
- baseClass->sptrDerivedClass = derivedClass;
- derivedClass->sptrBaseClass = baseClass;
- }
- int test()
- {
- InitData();
- return 0;
- }
smart pointer用來做class的data member的話,比起dumb pointer來方便很多:如不用擔心因此而產生的野指針的存在,也不用擔心資源的管理操作。
這段看似正常的代碼在test完了後的結果就是InitData中的類對象都在程序結束前一直不會被正常釋放,因為其baseClass和derivedClass一直占用著對方而使其引用計數永遠不會為0。如果因此而試圖將所有的shared_ptr改為auto_ptr,那麼結果會更慘,比如將上述部分代碼改為這樣:
- class BaseClass
- {
- public:
- auto_ptr<DerivedClass> sptrDerivedClass;
- };
- class DerivedClass
- {
- public:
- auto_ptr<BaseClass> sptrBaseClass;
- };
- void InitData()
- {
- auto_ptr<BaseClass> baseClass(new BaseClass);
- auto_ptr<DerivedClass> derivedClass(new DerivedClass);
- baseClass->sptrDerivedClass = derivedClass;
- derivedClass->sptrBaseClass = baseClass;
- }
在InitData的這一句:derivedClass->sptrBaseClass = baseClass 時候其實derivedClass所管理的指針已經為NULL了,試圖對NULL進行引用會Teminate了當前程序。但至少在debug狀態下,teminate前出現的assert信息能幫助我們知道自己不小心進行了循環引用,如此便能改正錯誤。倘若非要使用這樣的操作而且還想避免循環引用,那麼使用weak_ptr可以進行完美改善後面會講到)。
對於shared_ptr,說到這裡就差不多了,最後對於面試中常問到的shared_ptr的線程安全性,boost類庫實現的shared_ptr的文檔中有這麼一句:
shared_ptr objects offer the same level of thread safety as built-in types. A shared_ptr instance can be "read " (accessed using only const operations) simultaneously by multiple threads. Different shared_ptr instances can be "written to " (accessed using mutable operations such as operator= or reset) simultaneosly by multiple threads (even when these instances are copies, and share the same reference count underneath.)
Any other simultaneous accesses result in undefined behavior
即可以放心的像內置類型數據一樣在線程中使用shared_ptr。究其源碼,我看到的結果是只對counting機制實現了線程安全性,VS下的tr1庫counting機制的線程安全實現宏如下:
- #ifndef _DO_NOT_DECLARE_INTERLOCKED_INTRINSICS_IN_MEMORY
- extern "C" long __CLRCALL_PURE_OR_CDECL _InterlockedIncrement(volatile long *);
- extern "C" long __CLRCALL_PURE_OR_CDECL _InterlockedDecrement(volatile long *);
- extern "C" long __CLRCALL_PURE_OR_CDECL _InterlockedCompareExchange(volatile long *,
- long, long);
- #pragma intrinsic(_InterlockedIncrement)
- #pragma intrinsic(_InterlockedDecrement)
- #pragma intrinsic(_InterlockedCompareExchange)
- #endif /* _DO_NOT_DECLARE_INTERLOCKED_INTRINSICS_IN_MEMORY */
- #define _MT_INCR(mtx, x) _InterlockedIncrement(&x)
- #define _MT_DECR(mtx, x) _InterlockedDecrement(&x)
- #define _MT_CMPX(x, y, z) _InterlockedCompareExchange(&x, y, z)
如此的話,回答安全或者不安全都是含糊不清的。在我看來只能這樣說:shared_ptr對counting機制實現了線程安全,在多線程中使用多個線程共享的shared_ptr而不做其它任何安全管理機制,同樣會存在搶占資源而導致的一系列問題,但reference counting是永遠正常進行的。。
相應於shared_ptr所引發的循環引用而生的weak_ptr
對於打破shared_ptr的循環引用的一個最好的方法就是使用weak_ptr,它所提供的功能類似shared_ptr,但相對於shared_ptr來說,其功能卻弱很多,如同它的名字一樣。以下是一份主流weak_ptr所應有的接口聲明:
- template<class Ty> class weak_ptr {
- public:
- typedef Ty element_type;
- weak_ptr();
- weak_ptr(const weak_ptr&);
- template<class Other>
- weak_ptr(const weak_ptr<Other>&);
- template<class Other>
- weak_ptr(const shared_ptr<Other>&);
- weak_ptr& operator=(const weak_ptr&);
- template<class Other>
- weak_ptr& operator=(const weak_ptr<Other>&);
- template<class Other>
- weak_ptr& operator=(shared_ptr<Other>&);
- void swap(weak_ptr&);
- void reset();
- long use_count() const;
- bool expired() const;
- shared_ptr<Ty> lock() const;
- };
weak_ptr中沒有重載*和->操作符,因而不能通過它來訪問元素。看起來更像是一個shared_ptr的觀察者,如果用MVC架構來解釋的話,weak_ptr就充當了view層,而shared_ptr充當了model層,因為對於shared_ptr的任何操作後的狀態信息都可以通過其對應的weak_ptr來觀察出來,而觀察的同時自身卻並不做任何具體操作例如訪問元素或進行counting),事實上,倘若weak_ptr也提供類似的訪問操作的話,那麼意味著每次訪問都會改變count的值,如此以來,weak_ptr也就失去了其自身存在的價值。。 如果用weak_ptr來改善在上面闡述shared_ptr中的問題的話,只需將BaseClass或DerivedClass中任何一個類中的shared_ptr改為weak_ptr即可注意不能全部改成weak_ptr),看起來情況好了很多,但如此所帶來的問題是:程序員需提前預知將會發生的循環引用,如果不能提前預知呢?那就等待著內存洩露時刻的到來而自己卻全無所知,因為表象上看起來程序的確沒有任何異常情況。。
後記
對於auto_ptr這樣的RAII class的使用,不得不說其所帶來的繁瑣程度不亞於其所帶來的便利性,而對於其是否值得使用,Scott Meyers在《more effective c++》中給出的建議是:“靈巧指針應該謹慎使用, 不過每個C++程序員最終都會發現它們是有用的”,對於這一點,雖然我沒有過由於大量使用其而帶來很多束手無策的經驗,但對於其內部原理的剖析足以讓我望而生畏。。相對來說,shared_ptr卻顯得更人性些,但通過使用一個類來管理普通的dumb pointer,方便的同時所帶來的資源消耗也不可小視,畢竟任何一個dumb pointer只占一個字節,而一個shared_ptr所造就的資源消耗卻大了很多。通常情況下,對於一些經常使用的相同資源而卻有很多pointer訪問的情況,使用shared_ptr無疑是最好的適用場景了。對於由於使用shared_ptr所帶來的環狀引用而造就的內存洩露,weak_ptr確實能幫助全然解決困難,但當我們面對或寫下成千上萬行的代碼時,我想沒人能保證絕對能提前知曉所存在的所有環狀引用。。
無論如何,不存在一個足夠通用的RAII class能完全替代dumb pointer,唯有在能預知使用其而所帶來的便利性遠遠大於其所帶來的繁瑣度的情況下,其使用價值也就值得肯定了。而reference counting的思想在現在主流的跨平台2d游戲引擎cocos2d-x中已被展現的淋漓至盡。或許我以後的博客中,會有更多cocos2d-x方面的文章。。
本文出自 “酋長 ” 博客,請務必保留此出處http://clement.blog.51cto.com/2235236/772929