前言
前幾天在很多地方老是碰到RAII(Resouce 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
- __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
- 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
- #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方面的文章。。
轉載于:https://blog.51cto.com/clement/772929