智能Smart Pointer
智能指针的原理及实现
当类中有指针成员时,一般有两种方式来管理指针成员:一是采用值型的方式管理,每个类对象都保留一份指针指向的对象的拷贝;另一种更优雅的方式是使用智能指针,从而实现指针指向的对象的共享。
智能指针(smart pointer)的一种通用实现技术是使用引用计数(reference count)。智能指针类将一个计数器与类指向的对象相关联,引用计数跟踪该类有多少个对象共享同一指针。
每次创建类的新对象时,初始化指针并将引用计数置为1;当对象作为另一对象的副本而创建时,拷贝指针并增加与之相应的引用计数;对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数;调用析构函数时,析构函数减少引用计数(如果引用计数减至0,则删除基础对象)。
实现引用计数有两种经典策略:一是引入辅助类,二是使用句柄类。下面分别介绍这些内容
问题描述
假设有一个名为TestPtr的类,里面有一个指针成员,简化为如下代码
class TestPtr
{
public:
TestPtr(int *p): ptr(p) { }
~TestPtr( ) { delete ptr; }
// other operations
private:
int *ptr;
// other data
};
在这种情况下,类TestPtr对象的任何拷贝、赋值操作都会使多个TestPtr对象共享相同的指针。但在一个对象发生析构时,指针指向的对象将被释放,从而可能引起悬垂指针。
现在我们使用引用计数来解决这个问题,一个新的问题是引用计数放在哪里。显然,不能放在TestPtr类中,因为多个对象共享指针时无法同步更新引用计数。
方案一
这里给出的解决方案是,定义一个单独的具体类(RefPtr)来封装指针和相应的引用计数。由于这个类只是用于对类TestPtr中的成员指针ptr进行了封装,无其它用途,所以把引用计数类RefPtr的所有成员均定义为private,并把类TestPtr声明为它的友元类,使TestPtr类可以访问RefPtr类。示例代码如下:
class RefPtr
{
friend class TestPtr;
int *ptr;
count;
RefPtr (int *p): ptr(p), count(1) {}
~RefPtr () {
delete ptr;
}
};
class TestPtr
{
public:
TestPtr(int *p): ptr(new RefPtr(p)) { }
TestPtr(const TestPtr& src): ptr(src.ptr) {
++ptr->count;
}
TestPtr& operator= (const TestPtr& rhs) {
// self-assigning is also right
++rhs.ptr->count;
if (--ptr->count == 0)
delete ptr;
ptr = rhs.ptr;
return *this;
}
~TestPtr() {
if (--ptr->count == 0)
delete ptr;
}
private:
RefPtr *ptr;
};
当希望每个TestPtr对象中的指针所指向的内容改变而不影响其它对象的指针所指向的内容时,可以在发生修改时,创建新的对象,并修改相应的引用计数。这种技术的一个实例就是写时拷贝(Copy-On-Write)。
这种方案的缺点是每个含有指针的类的实现代码中都要自己控制引用计数,比较繁琐。特别是当有多个这类指针时,维护引用计数比较困难。
方案二
为了避免上面方案中每个使用指针的类自己去控制引用计数,可以用一个类把指针封装起来。封装好后,这个类对象可以出现在用户类使用指针的任何地方,表现为一个指针的行为。我们可以像指针一样使用它,而不用担心普通成员指针所带来的问题,我们把这样的类叫句柄类。在封装句柄类时,需要申请一个动态分配的引用计数空间,指针与引用计数分开存储。实现示例如下
#include <iostream>
#include <stdexcept>
using namespace std;
#define TEST_SMARTPTR
class Stub
{
public:
void print() {
cout<<"Stub: print"<<endl;
}
~Stub(){
cout<<"Stub: Destructor"<<endl;
}
};
template <typename T>
class SmartPtr
{
public:
SmartPtr(T *p = 0): ptr(p), pUse(new size_t(1)) { }
SmartPtr(const SmartPtr& src): ptr(src.ptr), pUse(src.pUse) {
++*pUse;
}
SmartPtr& operator= (const SmartPtr& rhs) {
// self-assigning is also right
++*rhs.pUse;
decrUse();
ptr = rhs.ptr;
pUse = rhs.pUse;
return *this;
}
T *operator->() {
if (ptr)
return ptr;
throw std::runtime_error("access through NULL pointer");
}
const T *operator->() const {
if (ptr)
return ptr;
throw std::runtime_error("access through NULL pointer");
}
T &operator*() {
if (ptr)
return *ptr;
throw std::runtime_error("dereference of NULL pointer");
}
const T &operator*() const {
if (ptr)
return *ptr;
throw std::runtime_error("dereference of NULL pointer");
}
~SmartPtr() {
decrUse();
#ifdef TEST_SMARTPTR
std::cout<<"SmartPtr: Destructor"<<std::endl; // for testing
#endif
}
private:
void decrUse() {
if (--*pUse == 0) {
delete ptr;
delete pUse;
}
}
T *ptr;
size_t *pUse;
};
int main()
{
try {
SmartPtr<Stub> t;
t->print();
} catch (const exception& err) {
cout<<err.what()<<endl;
}
SmartPtr<Stub> t1(new Stub);
SmartPtr<Stub> t2(t1);
SmartPtr<Stub> t3(new Stub);
t3 = t2;
t1->print();
(*t3).print();
return 0;
}
Boost中的智能指针
智能指针是存储指向动态分配(堆)对象指针的类。除了能够在适当的时间自动删除指向的对象外,他们的工作机制很像C++的内置指针。智能指针在面对异常的时候格外有用,因为他们能够确保正确的销毁动态分配的对象。他们也可以用于跟踪被多用户共享的动态分配对象。
事实上,智能指针能够做的还有很多事情,例如处理线程安全,提供写时复制,确保协议,并且提供远程交互服务。有能够为这些ESP (Extremely Smart Pointers)创建一般智能指针的方法,但是并没有涵盖进来。
智能指针的大部分使用是用于生存期控制,阶段控制。它们使用operator->和operator*来生成原始指针,这样智能指针看上去就像一个普通指针。
这样的一个类来自标准库:std::。它是为解决资源所有权问题设计的,但是缺少对引用数和的支持。并且,std::auto_ptr在被复制的时候会传输所有权。在大多数情况下,你需要更多的和/或者是不同的功能。这时就需要加入smart_ptr类。
smart_ptr 类
在Boost中的智能指针有:
。scoped_ptr,用于处理单个对象的唯一所有权;与std::auto_ptr不同的是,scoped_ptr可以被复制。
。scoped_array,与scoped_ptr类似,但是用来处理数组的
。,允许共享对象所有权
。shared_array,允许共享数组所有权
scoped_ptr
scoped_ptr智能指针与std::auto_ptr不同,因为它是不传递所有权的。事实上它明确禁止任何想要这样做的企图!这在你需要确保指针任何时候只有一个拥有者时的任何一种情境下都是非常重要的。如果不去使用scoped_ptr,你可能倾向于使用std::auto_ptr,让我们先看看下面的代码:
auto_ptr MyOwnString?
(new string("This is mine to keep!"));
auto_ptr NoItsMine?(MyOwnString?);
cout << *MyOwnString << endl; // Boom
这段代码显然将不能编译通过,因为字符串的所有权被传给了NoItsMine。这不是std::auto_ptr的设计缺陷—而是一个特性。尽管如此,当你需要MyOwnString达到上面的代码预期的工作效果的话,你可以使用scoped_ptr:
scoped_ptr MyOwnString?
(new string("This is mine to keep for real!"));
// Compiler error - there is no copy constructor.
scoped_ptr TryingToTakeItAnyway?
(MyOwnString?);
scoped_ptr通过从boost::noncopyable继承来完成这个行为(可以查看Boost.utility库)。不可复制类声明并将赋值操作符声明为private类型。
scoped_array
scoped_array与scoped_ptr显然是意义等价的,但是是用来处理数组的。在这一点标准库并没有考虑—除非你当然可以使用std::vector,在大多数情况下这样做是可以的。
用法和scoped_ptr类似:
typedef tuples::tupleint> ArrayTuple?;
scoped_array MyArray?(new ArrayTuple?[10]);
tuples::get<0>(MyArray?[5]) ="The library Tuples is also part of Boost";
tuple是元素的集合—例如两倍,三倍,和四倍。Tuple的典型用法是从函数返回多个值。Boost Tuple库可以被认为是标准库两倍的扩展,目前它与近10个tuple元素一起工作。支持tuple流,比较,赋值,卸包等等。
当scoped_array越界的时候,delete[]将被正确的调用。这就避免了一个常见错误,即是调用错误的操作符delete。
shared_ptr
这里有一个你在标准库中找不到的—引用数智能指针。大部分人都应当有过使用智能指针的经历,并且已经有很多关于引用数的文章。最重要的一个细节是引用数是如何被执行的—插入,意思是说你将引用计数的功能添加给类,或者是非插入,意思是说你不这样做。Boost shared_ptr是非插入类型的,这个实现使用一个从堆中分配来的引用计数器。关于提供参数化策略使得对任何情况都极为适合的讨论很多了,但是最终讨论的结果是决定反对聚焦于可用性。可是不要指望讨论的结果能够结束。
shared_ptr完成了你所希望的工作:他负责在不使用实例时删除由它指向的对象(pointee),并且它可以自由的共享它指向的对象(pointee)。
void PrintIfString?(const any& Any) {
if (const shared_ptr* s =
any_cast >(&Any)) {
cout << **s << endl;
}
}
int main(int argc, char* argv[])
{
std::vector Stuff;
shared_ptr SharedString1?
(new string("Share me. By the way,
Boost.any is another useful Boost
library"));
shared_ptr SharedString2?
(SharedString1?);
shared_ptr SharedInt1?
(new int(42));
shared_ptr SharedInt2?
(SharedInt1?);
Stuff.push_back(SharedString1?);
Stuff.push_back(SharedString2?);
Stuff.push_back(SharedInt1?);
Stuff.push_back(SharedInt2?);
// Print the strings
for_each(Stuff.begin(), Stuff.end(),
PrintIfString?);
Stuff.clear();
// The pointees of the shared_ptr's
// will be released on leaving scope
// shared_ptr的pointee离开这个范围后将被释放
return 0;
}
any库提供了存储所有东西的方法[2]HYPERLINK "file:///C:Documents%20and%20SettingsAdministrator My%20Documents新建 CUJhtml20.04karlsson%22%20l"[4]。在包含类型中需要的是它们是可拷贝构造的(CopyConstructible),析构函数这里绝对不能引发,他们应当是可赋值的。我们如何存储和传递“所有事物”?无区别类型(读作void*)可以涉及到所有的事物,但这将意味着将类型安全(与知识)抛之脑后。any库提供类型安全。所有满足any需求的类型都能够被赋值,但是解开的时候需要知道解开类型。any_cast是解开由any保存着的值的钥匙,any_cast与的工作机制是类似的—指针类型的类型转换通过返回一个空指针成功或者失败,因此赋值类型的类型转换抛出一个异常(bad_any_cast)而失败。
shared_array
shared_array与shared_ptr作用是相同的,只是它是用于处理数组的。
shared_array MyStrings?( new Base[20] );
深入shared_ptr实现
创建一个简单的智能指针是非常容易的。但是创建一个能够在大多数编译器下通过的智能指针就有些难度了。而创建同时又考虑异常安全就更为困难了。Boost::shared_ptr这些全都做到了,下面便是它如何做到这一切的。(请注意:所有的include,断开编译器处理,以及这个实现的部分内容被省略掉了,但你可以在Boost.smart_ptr当中找到它们)。
首先,类的定义:很显然,智能指针是(几乎总是)模板。
template class shared_ptr {
公共接口是:
explicit shared_ptr(T* p =0) : px(p) {
// fix: prevent leak if new throws
try { pn = new long(1); }
catch (...) { checked_delete(p); throw; }
}
现在看来,在构造函数当中两件事情是容易被忽略的。构造函数是explicit的,就像大多数的构造函数一样可以带有一个参数。另外一个值得注意的是引用数的堆分配是由一个try-catch块保护的。如果没有这个,你得到的将是一个有缺陷的智能指针,如果引用数没有能够成功分配,它将不能正常完成它自己的工作。
~shared_ptr() { dispose(); }
析构函数执行另外一个重要任务:如果引用数下降到零,它应当能够安全的删除指向的对象(pointee)。析构函数将这个重要任务委托给了另外一个方法:dispose。
void dispose() { if (—*pn == 0)
{ checked_delete(px); delete pn; } }
正如你所看到的,引用数(pn)在减少。如果它减少到零,checked_delete在所指对象 (px)上被调用,而后引用数(pn)也被删除了。
那么,checked_delete执行什么功能呢?这个便捷的函数(你可以在Boost.utility中找到)确保指针代表的是一个完整的类型。在你的智能指针类当中有这个么?
这是第一个赋值运算符:
template shared_ptr& operator=
(const shared_ptr& r) {
share(r.px,r.pn);
return *this;
}
这是成员模版,如果不是这样,有两种情况:
1. 如果没有参数化复制构造函数,类型赋值Base = Derived无效。
2. 如果有参数化复制构造函数,类型赋值将生效,但同时创建了一个不必要的临时smart_ptr。
这再一次的展示给你为什么不应当加入你自己的智能指针的一个非常好的原因—这些都不是很明显的问题。
赋值运算符的实际工作是由share函数完成的:
void share(T* rpx, long* rpn) {
if (pn = rpn) { // Q: why not px = rpx?
// A: fails when both == 0
++*rpn; // done before dispose() in case
// rpn transitively dependent on
// *this (bug reported by Ken Johnson)
dispose();
px = rpx;
pn = rpn;
}
}
需要注意的是自我赋值(更准确地说是自我共享)是通过比较引用数完成的,而不是通过指针。为什么这样呢?因为它们两者都可以是零,但不一定是一样的。
template shared_ptr
(const shared_ptr& r) : px(r.px) { // never throws
++*(pn = r.pn);
}
这个版本是一个模版化的拷贝构造和函数。可以看看上面的讨论来了解为什么要这样做。
赋值运算符以及赋值构造函数在这里同样也有一个非模版化的版本:
shared_ptr(const shared_ptr& r) :
// never throws
px(r.px) { ++*(pn = r.pn); }
shared_ptr& operator=
(const shared_ptr& r) {
share(r.px,r.pn);
return *this;
}
reset函数就像他的名字那样,重新设置所指对象(pointee)。在将要离开作用域的时候,如果你需要销毁所指对象(pointee)它将非常方便的帮你完成,或者简单的使缓存中的值失效。
void reset(T* p=0) {
// fix: self-assignment safe
if ( px == p ) return;
if (—*pn == 0)
{ checked_delete(px); }
else { // allocate new reference
// counter
// fix: prevent leak if new throws
try { pn = new long; }
catch (...) {
// undo effect of —*pn above to
// meet effects guarantee
++*pn;
checked_delete(p);
throw;
} // catch
} // allocate new reference counter
*pn = 1;
px = p;
} // reset
这里仍然请注意避免潜在的内存泄漏问题和保持异常安全的处理手段。
这样你就有了使得智能指针发挥其“智能”的运算符:
// never throws
T& operator*() const { return *px; }
// never throws
T* operator->() const { return px; }
// never throws
T* get() const { return px; }
这仅仅是一个注释:有的智能指针实现从类型转换运算符到T*的转换。这不是一个好主意,这样做常会使你因此受到伤害。虽然get在这里看上去很不舒服,但它阻止了编译器同你玩游戏。
我记得是Andrei Alexandrescu说的:“如果你的智能指针工作起来和哑指针没什么两样,那它就是哑指针。”简直是太对了。
这里有一些非常好的函数,我们就拿它们来作为本文的结束吧。
long use_count() const
{ return *pn; } // never throws
bool unique() const
{ return *pn == 1; } // never throws
函数的名字已经说明了它的功能了,对么?
关于Boost.smart_ptr还有很多应当说明的(比如std::swap和std::less的特化,与std::auto_ptr榜定在一起确保兼容性以及便捷性的成员,等等),由于篇幅限制不能再继续介绍了。详细内容请参考Boost distribution ()的smart_ptr.hpp。即使没有那些其它的内容,你不认为他的确是一个非常智能的指针么?
- 扩展阅读:
-
- 1
- 2