C++11 标准库源代码剖析:连载之四

Smart Pointers

Smart pointer,也就是所谓的“智能指针”,是指那些能够自我管理生命周期的指针对象。C++ 11之前,标准库只提供了一种智能指针,即std::auto_ptr。不过std::auto_ptr用处有限,大家更多地是使用boost智能指针。随着时间推移,智能指针正在变得越来越流行,越来越重要,所以标准委员会在制定C++ 11标准时,将boost智能指针也纳入到标准中。

C++ 11标准库提供了四种智能指针:

  • unique_ptr
  • shared_ptr
  • weak_ptr
  • auto_ptr

auto_ptr因为功能有限,用处也有限,所以就不讲了。至于weak_ptr,它的实现方式和shared_ptr很像,甚至大部分代码是通用的,所以也不专门讲了。下面我们重点分析一下unique_ptrshared_ptr。

unique_ptr

顾名思义,unique_ptr就是一个原生指针独一无二的拥有者和管理者。当一个unique_ptr离开其作用域时,其管理的原生指针会被自动销毁。

标准库中定义了两种类型的unique_ptr

  1. unique_ptr<T>,管理通过new获得的原生指针。
  2. unique_ptr<T[]>,管理通过new[]获得的指针数组。

这两种智能指针的实现方式大同小异,我们主要分析unique_ptr<T>的源码。理解了unique_ptr<T>的实现原理,unique_ptr<T[]>的实现原理自然也就明了了。

unique_ptr的源代码

// file: <memory>

template<class _Tp, class _Dp = default_delete<_Tp> >
class unique_ptr {
public:
    typedef _Tp element_type;
    typedef _Dp deleter_type;
    typedef typename __pointer_type<_Tp, deleter_type>::type pointer;

private:
    __compressed_pair<pointer, deleter_type> __ptr_;

    // ...
};

unique_ptr的声明包含两个模板参数,第一个参数_Tp显然就是原生指针的类型。第二个模板参数_Dp是一个deleter,默认值为default_delete<_Tp>。default_delete是一个针对delete operator的函数对象:

// file: memory

template<class T>
struct default_delete {
    void operator()(T* ptr) const noexcept {
        delete ptr;
    }
};

注意这行代码:

typedef typename __pointer_type<_Tp, deleter_type>::type pointer;

__pointer_type是一个type trait,用来“萃取”出正确的指针类型。为了方便理解,大可以认为它和下面的代码是等价的:

typedef _Tp* pointer;

unique_ptr内部用__compressed_pair保存数据,__compressed_pair是一个“空基类优化”的pair,阅读源代码时,完全可以将它当做一个std::pair来对待。

这基本就是unique_ptr的全部声明,下面我们来看如何构造一个unique_ptr

// file: memory

template<class _Tp, class _Dp = default_delete<_Tp> >
class unique_ptr {
    // ...

public:
    // 默认构造函数,调用pointer的默认构造函数
    inline constexpr unique_ptr() noexcept {
        : __ptr_(pointer()) {
    }

    // 将一个nullptr转换为一个unique_ptr
    inline constexpr unique_ptr(nullptr_t) noexcept 
        : __ptr_(pointer()) {
    }

    // 拷贝构造函数,注意参数类型为pointer,而不是const point&
    inline explicit unique_ptr(pointer __p) noexcept 
        : __ptr_(std::move(__p)){ 
    }

    // 移动构造函数
    inline unique_ptr<unique_ptr&& __u) noexcept
        : __ptr_(__u.release(), 
          std::forward<deleter_type>(__u.get_deleter())) {
    }

    // 移动赋值
    inline unique_ptr& operator=(unique_ptr&& __u) noexcept {
        reset(__u.release());
        __ptr_.second() = std::forward<deleter_type>(__u.get_deleter());
        return *this;
    }

    inline ~unique_ptr(){reset();}

    // ...
};

unqiue_ptr还定义了两个很重要的函数:reset(pointer)release()。reset(pointer)的功能是用一个新指针替换原来的指针,而release()则是是放弃原生指针的所有权。

// file: memory

template<class _Tp, class _Dp = default_delete<_Tp> >
class unique_ptr {
    // ...

public:

    // 放弃对原生指针的所有权,并返回原生指针
    inline pointer release() noexcept {
        pointer __t = __ptr_.first();
        __ptr_.first() = pointer();
        return __t;
    }

    // 用__p替换原生指针,被替换的指针最终被销毁
    inline void reset(pointer __p = pointer()) noexcept {
        pointer __tmp = __ptr_.first();
        __ptr_.first() = __p;
        if (__tmp)
            __ptr_.second()(__tmp);
    }

到目前为止,unique_ptr还不像个指针,因为还缺少两个方法:operator*operator->

// file: memory

template<class _Tp, class _Dp = default_delete<_Tp> >
class unique_ptr {
    // ...

public:
    inline add_lvalue_reference<_Tp>::type operator*() const {
        return *__ptr_.first();
    }

    inline pointer operator->() const noexcept {
        return __ptr_.first();
    }

这几乎就是unique_ptr的全部源代码了,总的来说比较容易理解。下面我们来分析一个稍微复杂一些的智能指针:shared_ptr

shared_ptr

还是先从声明入手:

// file: memory

template<class _Tp>
class shared_ptr {
public:
    typedef _Tp element_type;

private:
    element_type *__ptr_;
    __shared_weak_count* __cntrl_;

    // ...
};

shared_ptr内部维护了两个指针:一个是被其管理原生指针__ptr_,还有一个类型为__shared_weak_count的指针__cntrl_。那么这个__shared_weak_count又是什么呢?

file: memory

class __shared_count {
    // not copy constructible and not assignable
    __shared_count(const __shared_count&);
    __shared_count& operator=(const __shared_count&);

protected:
    long __shared_owners_; // how many owners do I have?
    virtual ~__shared_count();

public:
    explicit __shared_count(long __refs = 0) noexcept 
        : __shared_owners(__refs){}

        void __add_shared() noexcept;
        bool __release_shared() noexcept;
};

class __shared_weak_count : private __shared_count {
    long __shared_weak_owners_;

public:
    explicit __shared_weak_count(long __refs = 0) noexcept {
        : __shared_count(__refs), 
          __shared_weak_owners(__refs) {}

protected:
    virtual ~__shared_weak_count();

public:
    void __add_shared() noexcept;
    void __add_weak() noexcept;
    void __release_shared() noexcept;
    void __release_weak() noexcept;
    long use_count() const noexcept { return __shared_count::use_count();}

private:
    virtual void __on_zero_shared_weak() noexcept = 0;
};

__shared_weak_count是个虚基类,从它声明的类成员可以看出,这个类的作用应该是管理引用计数。实际上,shared_ptrweak_ptr内部都声明了__shared_weak_count*类型的成员变量,也就是说,__shared_weak_count同时管理shared ownershared weak owner,我个人认为这种做法值得商榷。

虚基类的作用类似于接口,是没法直接使用的,所以还必须定义一个“实在”类:

// file: memory

template<class _Tp, class _Dp, class _Alloc>
class __shared_ptr_pointer : public __shared_weak_count {
    __compressed_pair<__compressed_pair<_Tp, _Dp>, _Alloc> __data_;

public:
    inline __shared_ptr_pointer(_Tp __P, _Dp __d, _Alloc __a)
        : __data_(__compressed_pair<_Tp, _Dp>(__p, std::move(__d)), std::move(__a)) {}

    // ...

};

后面我们会看到,__shared_ptr_pointer正是shared_ptr的大内总管,不仅要记录shared_ptrshared owner,还要负责分配内存和销毁指针等工作。所以__shared_ptr_pointer类实际上有三个成员:

long __shared_owners_;
long __shared_weak_owners_;
__compressed_pair<__compressed_pair<_Tp, _Dp>, _Alloc> __data_;

理解了__shatrf_ptr_pointer,shared_ptr的源代码就容易读了:

// file: memory

template<class _Tp>>
class shared_ptr {
public:
    typedef _Tp element_type;

private:
    element_type*           __ptr_;
    __shared_weak_count*    __cntrl_;

    struct __nat{int __for_bool_;}; // placeholder

    // ...
};


// 如果 Yp* 能够转换成 _Tp*,则可以由 _Yp* 构造一个shared_ptr<_Tp>
template<class _Tp>
template<class _Yp>
shared_ptr<_Tp>::shared_ptr(_Yp* __p,
        typename enable_if<is_convertible<_Yp*, element_type*>::value, __nat>::type)
        : __ptr_(__p) {
        unique_ptr<_Yp> __hold(__p);
        typedef __shared_ptr_pointer<_Yp*, default_delete<_Yp>, allocator<_Yp> >_CntrBlk;
        __cntrl_ = new _CntrBlk(__p, default_delete<_Yp>(), allocator<_Yp>());
        __hold.release();
}

// copy constructor, increment reference count
template<class _Tp>
inline shared_ptr<Tp>::shared_ptr(const shared_ptr& __r) noexcept
    : __ptr(__r.__ptr_), __cntrl_(__r.__cntrl_) {
    if (__cntrl_)
        __cntrl_->__add_shared();
}

// move constructor, does't increment reference count
template<class _Tp>
inline shared_ptr<T>::shared_ptr(shared_ptr&& __r) noexcept
    : __ptr_(__r.__ptr_), __cntrl_(__r.__cntrl) {
        __r.__ptr_ = 0;
        __r.__cntrl_ = 0;
}

template<class _Tp>
shared_ptr<_Tp>::~shared_ptr(){
    if (__cntrl_)
        __cntrl_->__release_shared();
}

shared_ptr的实现虽然比unique_ptr复杂了一些,但是如果你能读懂unique_ptr的源代码,那shared_ptr的源代码对你来说也不算个事。因为篇幅的关系,对shared_ptr的分析就到这里。最后顺便说说一个关于效率的话题,我们已经看到了,shared_ptr内部维护了两个指针,如果你直接调用构造函数,就想这样:

class Widget;

auto sp = shared_ptr<Widget>(new Widget());

这里实际分配了两次内存,第一次是调用new Widget()的时候,第二次则是在shared_ptr构造函数的内部构造__cntrl_的时候。分配内存是很昂贵的操作,所以标准库提供了make_shared()函数,让你一次分配全部所需的内存:

// file: memory

template<class _Tp, class ..._Args>
inline
typename enable_if<!is_array<_Tp>::value, shared_ptr<_Tp>>::type
make_shared(_Args&& ...__args) {
    return shared_ptr<_Tp>::make_shared(std::forward<_Args>(__args)...);
}

template<class _Tp>
template<class ...Args>
shared_ptr<_Tp> shared_ptr<_Tp>::make_shared(_Args&& ...__args) {
    typedef __shared_ptr_emplace<_Tp, allocator<_Tp>>_CntrlBlk;
    typedef allocator<_CntrlBlk> _A2;
    typedef __alocator_destructor<_A2> _D2;

    _A2 __a2;
    unique_ptr<_CntrlBlk, _D2> __hold2(__a2.allocate(1), _D2(__a2, 1));
    ::new(__hold2.get()) _CntrlBlk(__a2, std::forward<_Args>(_args)...);
    shared_ptr<_Tp> __r;
    __r.__ptr_ = __hold2.get()->get();
    __r.__cntrl_ = __hold2.release();
    return __r;
}

我们可以看到,确实只分配了一次内存。注意内存的分配是在这里:

unique_ptr<_CntrlBlk, _D2> __hold2(__a2.allocate(1), _D2(__a2, 1));

而不是:

::new(__hold2.get()) _CntrlBlk(__a2, std::forward<_Args>(_args)...);

这里是调用placement new, 在__hold2的地址上构造一个__CntrBlk。__CntrBlk的类型是__shared_ptr_emplace<T, Alloc>,它的定义如下:

// file: memory

template<class _Tp, class _Alloc>
class __shared_ptr_emplace : public __shared_weak_count {
    __compressed_pair<_Alloc, _Tp> __data_;

public:
    template<class ..._Args>
    __shared_ptr_emplace(_Alloc __a, _Args&& ...__args)
        : __data_(piecewise_construct, std::forward_as_typle(__a),
        std::forward_as_tuple(std::forward<_Args>(__args)...)){}

    // ...
};

可见make_shared()shared_ptr的成员打包到一个__shared_ptr_emplace中,一次性在堆中构造出一个__shared_ptr_emplace,然后再拆包分配给shared_ptr的成员变量。

?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352

推荐阅读更多精彩内容