C++版本的sfntly——智能指针

作者: veaxen 分类: sfntly 发布时间: 2018-11-10 11:31

在研究谷歌的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_castdynamic_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()了。

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.