C++版本的sfntly——智能指针
在研究谷歌的sfntly项目的C++版本时,发现其智能指针跟我之前学习C++时接触的智能指针有所不同,是从另外一个角度实现的。本文就简单记录下。
使用
从sfntly源码文件中的注释可以知道:sfntly中的智能指针的实现是参照COM中的智能指针。要使用Ptr<>
智能指针的类对象需要继承自RefCounted<>
。
注释中的例子:
class Foo : public RefCountedM<Foo> {
public:
static Foo* CreateInstance() {
Ptr<Foo> obj = new Foo(); //引用计数为1
return obj.Detach();
}
};
typedef Ptr<Foo> FootPtr;
FooPtr obj;
obj.Attach(Foo::CreateInstance()); //引用计数为1
{
FooPtr obj2 = obj;//引用计数为2
} //obj2析构,引用计数为1
obj.Release(); //引用计数为0, 对象析构,内存回收
当一个抽象类的子类是确定要使用该智能指针的,那么该抽象类需要虚继承ReCount
类,这样抽象类的子类则要在继承了ReCounted<>
之后才可以正常使用,这就保证了所有继承自此抽象类的类都一致的使用智能指针,而且虚继承的方式还能减少重复继承带来的对象内存增加。
当在复合继承的时候,需要特别小心,例如:
class A : public RefCounted<A>;
class B : public A, publice RefCounted<B>;
在这种情况下,智能指针就没办法做到完美的管理对象的生命周期了。因为对象存在两份RefCounted<>
,这种情况下,引用计数的计算就不能保持都一致了。比如:
Ptr<B> obj = new B(); // RefCount<B>中记录引用计数为1
{
Ptr<A> obj2 = dynamic_cast<A*>(obj); // RefCount<A>中记录引用计数为1
} // obj2析构,RefCount<A>引用计数为0,释放对象
obj.Release(); // 重复释放内存,程序崩溃
所以上面的代码要怎么重构?
class I ; // 抽象类
class A : public I, public RefCounted<A>;
class B : public I, public RefCounted<B>;
将原本A、B都需要且相同的部分抽离出来,单独实现一个类I,然后新的A、B分别继承I,避免重复继承RefCounted<>
。
最后,sfntly中,原则上(推荐、默认)是这样将堆上的对象作为返回值的:
CALLER_ATTACH Foo* createFoo() { FooPtr obj = new Foo(); return obj.Detach(); }
CALLER_ATTACH Foo* passThrough() { FooPtr obj = createFoo(), return obj; }
FooPtr end_scope_pointer;
end_scope_pointer.Attach(passThrough());
这里的CALLER_ATTACH
是一个空的宏,主要是要表明,如果使用者要直接使用此函数,需要调用Ptr<T>::Attach()
,除非你明确知道你要做什么,比如passThrough
这个函数,没有使用obj.Attach(createFoo())
的方式来接管对象的生命周期,单纯只是做一个对象传递。
“Attach” or “operator=”
我们来看看什么时候我们要使用Ptr<T>::Attach()
,什么时候我们可以直接使用等号赋值,通过源码来了解他们的区别:
T* operator=(T* pT) {
if (p_ == pT) {
return p_;
}
if (pT) {
RefCount* p = static_cast<RefCount*>(pT);
if (p == NULL) {
return NULL;
}
p->AddRef(); // 增加新对象的引用计数
}
Release(); // 释放原来持有的对象,如果原来的对象引用计数减到0,则销毁对象
p_ = pT;
return p_;
}
再来看看Ptr<T>::Attach()
的实现:
void Attach(T* pT) {
if (p_ != pT) {
Release();
p_ = pT;
}
}
通过源码实现可以知道,operator=
会增加新对象的引用计数,而Ptr<T>::Attach()
则只是简单的释放原来持有的对象,然后将新对象的指针赋值给p_
,并没有增加新对象的引用计数。至于为什么要用这种方式来区分,本人水平有限,不知道背后的真正原因,但根据我的理解,可能是为了减少对引用计数的操作(原子操作,存在性能损失)
那么既然有这种区分,那么也要充分理解其代码上的含义及用法。
首先Ptr<T>::Attach()
是和Ptr<T>::Detach()
配合使用的,当一个智能指针调用Detach()
时,智能指针不会去减少对象的引用计数,而是直接将p_
置空,然后返回对象的指针,这意味着,该智能指针交出对象的生命周期控制权,那么我们需要用Attach()
去接这个对象的指针,代表这一个新的智能指针接管了控制权。
CALLER_ATTACH Foo* createFoo() {
FooPtr obj = new Foo(); //引用计数为1
return obj.Detach(); // 交出对象控制权,此时引用计数还是为1
} //obj析构,但是不会去释放new Foo()创建的对象,因为已经交出控制权
FooPtr end_scope_pointer;
end_scope_pointer.Attach(createFoo()); //接管对象
理解了Ptr<T>::Attach()
和Ptr<T>::Detach()
之后,operator=
应该很容易就明白了,使用operator=
会在原来的引用计数的基础上再+1。
对比shared_ptr
对比一下sfntly中的智能指针和C++11的share_ptr<T>
,可以发现,要使用Ptr<T>
的类对象都必须要继承RefCounted<T>
,而share_ptr<T>
则没有这个限制,这是因为对引用计数的实现方式的区别造成的。
share_ptr<T>
要灵活和方便得多,而且也不会因为重复继承问题导致引用计数计算混乱,如前面一开始提到的重复继承的情况,shared_ptr
有两种类型转换的函数,一个是 static_pointer_cast
, 一个是 dynamic_pointer_cast
其实用法真的和 C++ 提供的static_cast
和dynamic_cast
很像:
shared_ptr<A> ptra;
shared_ptr<B> ptrb(new B());
ptra = dynamic_pointer_cast<A>(ptrb);
但是shared_ptr<T>
的构造要求比较高,一定要统一使用shared_ptr<T>
来引用管理对象指针,因为引用计数没有跟对象实现强一对一绑定,更多是要求程序员遵守规范,而Ptr<T>
的引用计数就保持在对象中,可以一对一对应,也就可以实现Ptr<T>::Attach()
和Ptr<T>::Detach()
了。