主流RAII class的存在价值——..
前言
前几天在很多地方老是碰到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 /* _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; }
对于第三点,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; }
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; };
后记
对于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方面的文章。。