More Effective C++ 条款28 Smart Pointers(智能指针)
1. 智能指针(如标准库的auto_ptr,shared_ptr,weak_ptr,boost的scoped_ptr等)主要用于动态内存的管理,同时提供给用户与内置指针一样的使用方法,本条款主要涉及智能指针在构造与析构,复制和赋值,解引等方面的注意点,而非智能指针的实现细节.
2. 智能指针的构造,赋值,析构
智能指针的copy constructor,assignment operator,destructor对应于不同的观念而有不同的实现,主要有三种选择:
1).不允许对象的共享,在调用copy constructor和assignment时转移对象所有权,这样在调用destructor时就可以直接delete智能指针内含的内置指针,如标准库的auto_ptr,其实现可能像这样:
template<class T> class auto_ptr { public: ... auto_ptr(auto_ptr<T>& rhs); auto_ptr<T>& operator=(auto_ptr<T>& rhs); ... }; template<class T> auto_ptr<T>::auto_ptr(auto_ptr<T>& rhs) { pointee = rhs.pointee; rhs.pointee = 0; // 转移对象所有权 } template<class T> auto_ptr<T>& auto_ptr<T>::operator=(auto_ptr<T>& rhs) { if (this == &rhs) // 自我赋值的情况 return *this; delete pointee; pointee = rhs.pointee; // 转移对象所有权 rhs.pointee = 0; return *this; }
值得注意的是,由于auto_ptr的copy constructor被调用时,对象所有权便转移了,像这样:
void printTreeNode(ostream& s, auto_ptr<TreeNode> p) { s << *p; } int main(){ auto_ptr<TreeNode> ptn(new TreeNode); ... printTreeNode(cout, ptn); //通过传值方式传递auto_ptr ... }
调用printTreeNode后,ptn所指向的内存便被释放,内含的内置指针也被置为0,但这并不符合用户的预期.这说明不能使用pass-by-value的方式传递auto_ptr给函数,只能使用pass-by-reference.
使用这种策略实现的智能指针的destructor可能像这样:
template<class T> SmartPtr<T>::~SmartPtr() { if (*this owns *pointee) { delete pointee; } }
2). 不允许对象的共享,调用copy constructor和assignment operator时进行深度拷贝——产生新对象.这种设计思想较简单,缺点也较明显:新对象的产生需要消耗资源.这里不再讨论
3). 允许对象的共享,使用引用计数,调用copy constructor和assignment operator增加引用计数的个数.当引用计数的个数为0时便析构对象并释放内存,如标准库的shared_ptr,关于引用计数的具体实现见条款29.
3. 实现Dereference Operators(解引操作符)
主要讨论operator*和operator->的实现,前者放回所指对象的引用,像这样:
template<class T> T& SmartPtr<T>::operator*() const { perform "smart pointer" processing; return *pointee; }
如果程序采用了lazy fetching(见条款17)策略,就有可能需要为pointers变换出一个新对象.需要注意的是,operator*返回的是引用,如果返回对象,可能会产生由于SmartPtr指向的是T的派生类对象而非T类对象而造成的切割问题.
operator->和operator*类似,operator->返回指针.
对于使用引用计数的shared_ptr,问题还未停止,它允许多个智能指针共享相同对象,但前提是这些指针所指向的对象相同.由于operator*和operator->返回所指对象的引用和指针,这可能导致其所指对象被更改,但原则上共享同一块内存的其他智能指针却要求所指对象保持不变.因此有必要在调用operator*和operator->的时候开辟一块新内存,使调用operator*和operator->的智能指针指向这块新内存以防止共享内存被篡改,像这样:
template<class T> T& SmartPtr<T>::operator*() const{ if(number of reference!=1){ pointee=new T(*pointee); --reference number of the old object; set the reference number of the new object to 1; } return *pointee; }
4. 测试Smart Pointers是否为Null
直接的策略是定义隐式转换操作符operator void*,使得以下操作可以通过编译:
if (ptn == 0) ... // 正确 if (ptn) ... // 正确 if (!ptn) ... //正确
但隐式转换操作符很容易被滥用,它使得不同类型的指针可以相比较,以下代码可以通过编译:
SmartPtr<Apple> pa; SmartPtr<Orange> po; ... if (pa == po) ...//可以通过编译
尽管pa和po是不同类型的智能指针,但由于没有定义Smart<Apple>和Smart<Orange>为参数的operator==,因此编译器默认调用operator void*,使得以上代码通过编译.
一种差强人意的方法是允许测试null,但使用!操作符,如果内置指针为null,便返回true,客户端要测试智能指针是否为null,就要像这样:
SmartPtr<TreeNode> ptn; ... if (!ptn) { ... } else { ... }
但以下做法却被禁止:
if (ptn == 0) ... if (ptn) ...
iostream程序库不仅允许隐式类型转换void*,还提供operator!,C++标准库中,"隐式转换为void*"已被"隐式转换为bool"取代,而operator bool总是返回operator!的反.
5. 将Smart Pointers转换为Dumb Pointers
有时要兼容并未使用智能指针的程序库,就要允许智能指针到内置指针的转换,直接的思路还是隐式转换操作符:
template<class T> class DBPtr { public: ... operator T*() { return pointee; } ... };
但是正如多次强调的,隐式转换操作符很容易被滥用,它使得客户可以轻易获得内置指针,从而绕过智能指针的控制,像这样:
class Tuple{...}; void processTuple(DBPtr<Tuple>& pt) { Tuple *rawTuplePtr = pt; // 得到内置指针 use rawTuplePtr to modify the tuple }
像这样的操作也会被通过:
DBPtr<Tuple> pt=new Tuple; delete pt;//通过,执行隐式类型转换
但这几乎肯定会造成错误,因为pt的析构函数执行时可能再次delete同一块内存.
此外,对于采用引用计数的实现版本来说,"允许clinets直接使用dumb pointers"往往会导致导致簿记方面的错误,造成严重后果,
即使实现了隐式转换操作符,但它还是不能做到提供和内置指针完全一样的行为,因为编译器禁止连续隐式调用自定义的隐式类型转换,像这样的使用会失败:
class TupleAccessors { public: TupleAccessors(const Tuple *pt); // Tuple到TupleAccessor的转换 ... }; TupleAccessors merge(const TupleAccessor& ta1,const TupleAccessors& ta2); DBPtr<Tuple> pt1, pt2; ... merge(pt1,pt2);//调用会出错
尽管DBPtr<Tuple>到Tuple*再到TupleAccessor的转换就可以匹配merge的参数,但编译器禁止这么做.
解决方法是使用普通成员函数进行显式转换以代替隐式转换操作符,像这样:
class DBPtr { public: ... T* toPrimary() { return pointee; } ... };
6. Smart Pointers和"与继承有关的"类型转换
两个类之间有继承关系,但以这两个类为参数具现化的类模板却没有继承关系,由于智能指针是类模板,因此智能指针的包装会屏蔽内置指针的继承关系,例如对于以下继承层次:
class MusicProduct { public: MusicProduct(const string& title); virtual void play() const = 0; virtual void displayTitle() const = 0; ... }; class Cassette: public MusicProduct { public: Cassette(const string& title); virtual void play() const; virtual void displayTitle() const; ... }; class CD: public MusicProduct { public: CD(const string& title); virtual void play() const; virtual void displayTitle() const; ... } void displayAndPlay(const MusicProduct* pmp, int numTimes) { for (int i = 1; i <= numTimes; ++i) { pmp->displayTitle(); pmp->play(); }
整个继承体系像这样:
由于各个类的继承关系,可以利用指针的多态实现面向对象编程,像这样:
Cassette *funMusic = new Cassette("Alapalooza"); CD *nightmareMusic = new CD("Disco Hits of the 70s"); displayAndPlay(funMusic, 10); displayAndPlay(nightmareMusic, 0);
但当指针经过封装成为智能指针之后,正如开始所说,以下代码将无法通过编译:
void displayAndPlay(const SmartPtr<MusicProduct>& pmp,int numTimes); SmartPtr<Cassette> funMusic(new Cassette("Alapalooza")); SmartPtr<CD> nightmareMusic(new CD("Disco Hits of the 70s")); displayAndPlay(funMusic, 10); // 错误! displayAndPlay(nightmareMusic, 0); // 错误!
这是由于MusicProduct,Cassette,CD之间有继承关系,但智能指针SmartPtr<MusicProduct>,SmartPtr<Cassette>,SmartPtr<CD>之间却没有内在的继承关系.
最直接的解决方法是为每一个智能指针类定义一个隐式类型转换操作符,像这样:
class SmartPtr<Cassette> { public: operator SmartPtr<MusicProduct>() { return SmartPtr<MusicProduct>(pointee); } ... private: Cassette *pointee; }; class SmartPtr<CD> { public: operator SmartPtr<MusicProduct>() { return SmartPtr<MusicProduct>(pointee); } ... private: CD *pointee; };
这种方法可以解决类型转换的问题,但是却治标不治本:一方面,必须为每一个智能指针实例定义隐式类型转换操作符,这无疑与模板的初衷背道相驰;另一方面,类的继承层次可能很庞大,采用以上方式,继承层次的最底层类的负担将会非常大——必须为对象直接或间接继承的每一个基类提供隐式类型转换操作符.
"将nonvirtual member function声明为templates"是C++后来接入的一个性质,使用它可以从根本上解决饮食类型转换的问题,像这样:
template<class T> class SmartPtr { public: SmartPtr(T* realPtr = 0); T* operator->() const; T& operator*() const; template<class newType> // 模板成员函数 operator SmartPtr<newType>() { return SmartPtr<newType>(pointee); } ... };
这个成员函数模板将智能指针之间的隐式类型转换交由底层内置指针来完成,保证了指针转换的"原生态":如果底层指针能够转换,那么包装后的智能指针也能够进行转换.唯一的缺点是它是通过指针之间的隐式类型转换来实现指针的多态,也就是说,它实际上并不能区分对象之间的继承层次,假如扩充MusicProduct的继承体系,加上一个新的CasSingle class,像这样:
那么对于以下代码:
template<class T> class SmartPtr { ... }; void displayAndPlay(const SmartPtr<MusicProduct>& pmp, int howMany); void displayAndPlay(const SmartPtr<Cassette>& pc, int howMany); SmartPtr<CasSingle> dumbMusic(new CasSingle("Achy Breaky Heart")); displayAndPlay(dumbMusic, 1);//错误,隐式类型转换函数的调用具有二义性
正如之前所言,使用隐式类型转换操作符实现的指针多态并不能区分对象的继承层次,也就是说将SmartPtr<CasSingle>转为SmartPtr<Cassette>&和转为SmartPtr<MusicProduct>&具有同样的优先级,因此造成二义性.而内置指针却能做到这一点,它优先将CasSingle绑定到Cassette&,因为CaSingle直接继承自Cassette.此外,以上策略还有移植性不高的缺点:有些编译器可能并不支持member templates.
7. Smart Pointers与const
对于内置指针,const修饰的含义因其位置而不同:
CD goodCD("Flood"); const CD *p; // p 是一个non-const 指针,指向 const CD 对象 CD * const p = &goodCD; // p 是一个const 指针,指向non-const CD 对象;因为 p 是const,它必须在定义时就被初始化 const CD * const p = &goodCD; // p 是一个const 指针,指向一个 const CD 对象
但对于智能指针,只有一个地方可以放置const,因此cosnt只能施行于指针之上,而不能施行于指针所指对象之上:
const SmartPtr<CD> p=&goodCD;
要是const修饰所值对象很简单,像这样:
SmartPtr<const CD> p=&goodCD;
由此方法可以实现和内置指针相同的四种指针:
SmartPtr<CD> p; // non-const 对象, non-const 指针 SmartPtr<const CD> p; // const 对象,non-const 指针 const SmartPtr<CD> p = &goodCD; // non-const 对象,const 指针 const SmartPtr<const CD> p = &goodCD; // const 对象,const 指针
但这种方法仍有缺陷,正如经由模板包装之后,有继承关系的两个类完全没有关系一样,经由智能指针模板包装后的const和non-const对象完全不同,像这样看起来理所当然的代码通不过编译:
SmartPtr<CD> pCD = new CD("Famous Movie Themes"); SmartPtr<const CD> pConstCD = pCD;
使用之前的隐式类型转换技术可以顺带解决这个问题,但又有所区别:const与non-const的转换是单向的,即可以对const指针做的事也可以对non-const指针进行,但可以对non-const指针做的事未必可以对const指针进行.这与public继承类似,利用这种性质,令每一个smart pointer-to-T-class public继承一个对应的smart pointer-to-const-T class:
template<class T> // 指向const 对象的 class SmartPtrToConst { protected: union { const T* constPointee; // 提供给SmartPtrToConst 访问 T* pointee; // 提供给SmartPtr 访问 }; }; template<class T> class SmartPtr: public SmartPtrToConst<T> { public: template<class constType> operator SmartPtrToConst<constType>(); ... //没有额外数据成员 };
SmartPtrToConst使用了union,这样constPointee和pointee共享同一块内存SmartPtrToConst使用constPointee,SmartPtr使用pointee.
现在,使用SmartPtrToConst和SmartPtr分别代表指向const和non-const对象的智能指针,以下代码可以通过编译:
SmartPtr<CD> pCD = new CD("Famous Movie Themes"); SmartPtrToConst<CD> pConstCD = pCD;
8. 从2-7的讨论可以看出,智能指针功能强大,但索要付出的代价也很高,此外,智能指针无论如何也不能完全替代内置指针.当然,尽管内置指针在实现和维护方面需要大量技巧,但与其强大的功能相比在多数情况下还是值得的.