STL-智能指针三剑客源码阅读

智能指针出现很多, 但是自己用得很少. 本文从源码层面来学习智能指针, 学习是怎么实现的, 以及如此实现可以实现如何的功能.

unique_ptr

我认为unique_ptr是编译器强制人类某些行为的例子, 只允许人类这样做而不允许人类那样做. 可以参考explicit说明符的一些想法.

源码在这里.

其析构函数会释放内存资源:

1
2
3
4
5
6
7
~unique_ptr() noexcept
{
    auto& __ptr = _M_t._M_ptr();
    if (__ptr != nullptr)
        get_deleter()(__ptr);
    __ptr = pointer();
}

为了保证这一点, unique_ptr就不允许用户将一块内容"多人"使用, 所以需要限制用户的拷贝和赋值行为:

1
2
3
// Disable copy from lvalue.
unique_ptr(const unique_ptr&) = delete;
unique_ptr& operator=(const unique_ptr&) = delete;

禁用了左值拷贝构造和赋值, 这样可以保证只有一个unique_ptr指向一块内存, 不会有多个unique_ptr指向一块内存. 但是允许了右值拷贝构造和赋值:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
unique_ptr(unique_ptr&& __u) noexcept
: _M_t(__u.release(), std::forward<deleter_type>(__u.get_deleter())) { }

unique_ptr&
operator=(unique_ptr&& __u) noexcept
{
    reset(__u.release());
    get_deleter() = std::forward<deleter_type>(__u.get_deleter());
    return *this;
}

右值在构造结束后就会被销毁, 所以此处的右值构造可以保证只有一个unique_ptr指向一块内存. 在内存转移的时候使用的是release接口(和reset(__u._M_t)是有区别的), 因为内存转移时候需要保证原unique_ptr的数据指针为空, 不能指向需要转移的内存, 不然在临时变量析构的时候会释放这块内存. 所以release接口的作用就是提取数据内存的指针, 将本来数据指针置空, 返回数据内存指针:

1
2
3
4
5
6
7
pointer
release() noexcept
{
    pointer __p = get();
    _M_t._M_ptr() = pointer();
    return __p;
}

unique_ptr有太多行为限制, 除了行为限制, 比较容易想到的是使用计数器形式实现RAII.

shared_ptr

shared_ptr是基于计数器的智能指针, 继承自__shared_ptr, 自身没有实现任何引用计数的功能. shared_ptr源码

1
2
template<typename _Tp>
    class shared_ptr : public __shared_ptr<_Tp>

__shared_ptr 继承自 __shared_ptr_access.

1
2
3
template<typename _Tp, _Lock_policy _Lp>
    class __shared_ptr
    : public __shared_ptr_access<_Tp, _Lp>

__shared_ptr本身维护两个变量, 内容指针和引用计数器.

1
2
element_type*           _M_ptr;         // Contained pointer.
__shared_count<_Lp>     _M_refcount;    // Reference counter.

计数器的使用

以下看看__shared_ptr实现了哪些需要借助引用计数的方法:

  1. 拷贝构造

拷贝构造数据和计数器, 而计数器的拷贝构造会使得计数器的值+1.

1
__shared_ptr(const __shared_ptr&) noexcept = default;

右值构造, 相当于右值的数据和计数器给了左值, 右值获得了空的数据和0计数器. 因为右值本身就只有一个引用, 所以交换是可以的.

1
2
3
4
5
6
__shared_ptr(__shared_ptr&& __r) noexcept
: _M_ptr(__r._M_ptr), _M_refcount()
{
_M_refcount._M_swap(__r._M_refcount);
__r._M_ptr = 0;
}
  1. 复制操作

左值复制使用默认函数, 所以涉及到计数器的复制, 计数器复制操作也会设计+1操作.

1
__shared_ptr& operator=(const __shared_ptr&) noexcept = default;

右值复制同右值构造, 使用swap交换.

1
2
3
4
5
6
__shared_ptr&
operator=(__shared_ptr&& __r) noexcept
{
    __shared_ptr(std::move(__r)).swap(*this);
    return *this;
}

以上, 可以知道shared_ptr在左值构造和左值复制操作时会涉及计数器+1的操作.

计数器的实现

重点关注引用计数器的实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
template<_Lock_policy _Lp>
class __shared_count
{
public:
    constexpr __shared_count() noexcept : _M_pi(0)
    { }

    __shared_count(const __shared_count& __r) noexcept
    : _M_pi(__r._M_pi)
    {
        if (_M_pi != 0)
            _M_pi->_M_add_ref_copy();
    }

    __shared_count&
    operator=(const __shared_count& __r) noexcept
    {
        _Sp_counted_base<_Lp>* __tmp = __r._M_pi;
        if (__tmp != _M_pi)
        {
            if (__tmp != 0)
                __tmp->_M_add_ref_copy();
            if (_M_pi != 0)
                _M_pi->_M_release();
                _M_pi = __tmp;
        }
        return *this;
    }

    void
    _M_swap(__shared_count& __r) noexcept
    {
        _Sp_counted_base<_Lp>* __tmp = __r._M_pi;
        __r._M_pi = _M_pi;
        _M_pi = __tmp;
    }


//...
private:
    friend class __weak_count<_Lp>;
    _Sp_counted_base<_Lp>*  _M_pi;
}

引用计数器的拷贝构造和复制操作都涉及到了计数器的加减, 拷贝构造时计数器会默认+1, 而复制操作时可能会将=右边的计数器释放.

这里有个疑问, 为什么拷贝构造和复制操作的行为不一样呢?

因为拷贝构造时说明原本还没有构造计数器, 对应的就是shared_ptr的拷贝构造, 比如shared_ptr<int> p2(p1), 这时候p1p2都没有被释放, 是能够正常使用的, 所以拷贝构造时只需要计数器+1就行了. 复制操作需要释放是因为原本指向一个数据的指针会指向另外一个数据, 比如p2 = p1, p2原本可能指向p, 这时候变成了指向p1, 所以原来p的计数器需要-1, p1的计数器就需要+1.

以上计数器操作来自于_Sp_counted_base, 那么_Sp_counted_base是怎么实现的? 源码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
template<_Lock_policy _Lp = __default_lock_policy>
class _Sp_counted_base
: public _Mutex_base<_Lp>
{
    public:
        _Sp_counted_base() noexcept
        virtual
        ~_Sp_counted_base() noexcept
        { }

        : _M_use_count(1), _M_weak_count(1) { }

        void
        _M_add_ref_copy()
        { __gnu_cxx::__atomic_add_dispatch(&_M_use_count, 1); }
        //...

    private:
        _Sp_counted_base(_Sp_counted_base const&) = delete;
        _Sp_counted_base& operator=(_Sp_counted_base const&) = delete;
        _Atomic_word  _M_use_count;     // #shared
        _Atomic_word  _M_weak_count;    // #weak + (#shared != 0)
};

_Sp_counted_base维护了两个计数器, 一个用于shared一个用于weak, 并且两个都是原子变量, 如果关注源码, 还可以发现add或者release操作也是原子的, 并且release操作时会涉及内存屏障(TODO:内存屏障还不太了解).

同时,_Sp_counted_base的析构函数什么都没有做, 所以如果需要析构release计数器, 就依赖于上层函数的接口, 对应的就是:

1
2
3
4
5
__shared_count::~__shared_count() noexcept
{
if (_M_pi != nullptr)
    _M_pi->_M_release();
}

什么时候删除

一般猜测是析构函数的时候会delete数据, 但是并没有很容易地找到对应的代码, 所以这部分会介绍数据的delete到底是在哪里.

__shared_ptr的构造函数令人怀疑, 因为_M_refcount会需求一个__p参数来构造, 而__p代表了源数据.

1
2
3
4
5
6
7
8
template<typename _Yp, typename _Deleter, typename = _SafeConv<_Yp>>
__shared_ptr(_Yp* __p, _Deleter __d)
: _M_ptr(__p), _M_refcount(__p, std::move(__d))
{
    static_assert(__is_invocable<_Deleter&, _Yp*&>::value,
        "deleter expression d(p) is well-formed");
    _M_enable_shared_from_this_with(__p);
}

接下来看看__shared_count的构造函数, 一般会调用下面这个构造函数, 不一般的情况就不分析了…

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
template<typename _Ptr, typename _Deleter, typename _Alloc>
__shared_count(_Ptr __p, _Deleter __d, _Alloc __a) : _M_pi(0)
{
    typedef _Sp_counted_deleter<_Ptr, _Deleter, _Alloc, _Lp> _Sp_cd_type;
    __try
    {
        typename _Sp_cd_type::__allocator_type __a2(__a);
        auto __guard = std::__allocate_guarded(__a2);
        _Sp_cd_type* __mem = __guard.get();
        ::new (__mem) _Sp_cd_type(__p, std::move(__d), std::move(__a));
        _M_pi = __mem;
        __guard = nullptr;
    }
    __catch(...)
    {
        __d(__p); // Call _Deleter on __p.
        __throw_exception_again;
    }
}

这部分构造函数中包含了数据指针:

1
::new (__mem) _Sp_cd_type(__p, std::move(__d), std::move(__a));

_Sp_cd_type对应的_Sp_counted_deleter比较令我注意, 它继承自_Sp_counted_base, 并且会将_Sp_counted_deleter类型的数据赋值给_M_pi.

_Sp_counted_deleter的定义如下:

1
2
3
// Support for custom deleter and/or allocator
template<typename _Ptr, typename _Deleter, typename _Alloc, _Lock_policy _Lp>
class _Sp_counted_deleter final : public _Sp_counted_base<_Lp>

注意到, _M_pi对应的是如下:

1
_Sp_counted_base<_Lp>*  _M_pi;

在此之前我们分析了_Sp_counted_base的析构函数什么也没有做, 依赖于__shared_count的析构, 而__shared_count的析构会调用_M_release.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void
_M_release() noexcept
{
    // Be race-detector-friendly.  For more info see bits/c++config.
    _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_use_count);
    if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1)
    {
    _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_use_count);
    _M_dispose();
    // There must be a memory barrier between dispose() and destroy()
    // to ensure that the effects of dispose() are observed in the
    // thread that runs destroy().
    // See http://gcc.gnu.org/ml/libstdc++/2005-11/msg00136.html
    if (_Mutex_base<_Lp>::_S_need_barriers)
        {
        __atomic_thread_fence (__ATOMIC_ACQ_REL);
        }
    // Be race-detector-friendly.  For more info see bits/c++config.
    _GLIBCXX_SYNCHRONIZATION_HAPPENS_BEFORE(&_M_weak_count);
    if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count,
                                                -1) == 1)
        {
        _GLIBCXX_SYNCHRONIZATION_HAPPENS_AFTER(&_M_weak_count);
        _M_destroy();
        }
    }
}

我们只看shared引用部分, 计数器减少到0后会调用到_M_dispose, 这是一个虚函数, 所以会调用到子类的_M_dispose. 对应的则是_Sp_counted_deleter_M_dispose. 其内容为:

1
2
3
virtual void
_M_dispose() noexcept
{ _M_impl._M_del()(_M_impl._M_ptr); }

原来是在这里delete源数据的! 比较令我困惑的是, 删除数据的操作是在计数器对象里面的进行的.

循环引用

这是shared_ptr中谈论比较多的问题, 比如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <memory>

using namespace std;

class Father;
class Son;

class Father{
public:
    shared_ptr<Son> m_son;

    Father() {
        cout << __func__ << endl;
    };
    ~Father() {
        cout << __func__ << endl;
    };
};

class Son{
public:
    shared_ptr<Father> m_father;

    Son() {
        cout << __func__ << endl;
    };
    ~Son() {
        cout << __func__ << endl;
    };
};

int main(){
    shared_ptr<Father> father(new Father);
    shared_ptr<Son> son(new Son);

    father->m_son = son;
    son->m_father = father;

    cout << "father count " << father.use_count() << endl;
    cout << "son count " << son.use_count() << endl;
}

输出是:

1
2
3
4
Father
Son
father count 2
son count 2

只有构造没有析构, 因为在函数退出时引用计数器时2, 这时候就需要我们手动release一遍, 但是这明显不符合RAII的原则, 会导致shared_ptr四不象. 为应对这个问题, 设计了weak_ptr类.

weak_ptr

shared_ptr, weak_ptr的主要实现在__weak_ptr:

1
2
template<typename _Tp, _Lock_policy _Lp>
class __weak_ptr

没有发现__weak_ptr有任何基类. 关注__weak_ptr的构造可以发现, 是没有普通指针的构造接口的, 但是可以从weak_ptr或者shared_ptr构造.

这里关注两个常用的方法:

1
2
3
__shared_ptr<_Tp, _Lp>
lock() const noexcept
{ return __shared_ptr<element_type, _Lp>(*this, std::nothrow); }

lock方法会将weak指针转换为shared指针, 从而可以访问数据内存, 并且weak指针是不提供方法直接访问数据内存的.

1
2
3
long
use_count() const noexcept
{ return _M_refcount._M_get_use_count(); }

use_count方法返回数据内存的引用计数, _M_get_use_count实际返回的是shared计数, 而不是weak计数.

再来关注class __weak_count, 类似的, 在构造的时候会增加计数器:

1
2
3
4
5
6
__weak_count(const __shared_count<_Lp>& __r) noexcept
: _M_pi(__r._M_pi)
{
    if (_M_pi != nullptr)
        _M_pi->_M_weak_add_ref();
}

析构的时候会减少计数器:

1
2
3
4
5
~__weak_count() noexcept
{
    if (_M_pi != nullptr)
        _M_pi->_M_weak_release();
}

但是, 区别于shared指针的计数器, weak指针使用的是weak计数器, 目前来看weak计数器似乎没有用到, 仅在weak计数器为0的时候会释放weak_count自身.

以上, weak_ptr不会增加shared计数器, 会增加weak计数器, 不能直接访问weak_ptr指向的数据, 需要转换为share_ptr才能访问.

weak_ptr的构造决定了它一般是和shared_ptr配合使用的, 更像是担任数据缓存的角色(或者说数据快照), 它自身不维护数据的生命周期, 如果源数据被释放无法访问了, 那weak_ptr也将无法访问源数据, 比如shared_ptr循环引用问题, 可以这样改写:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <memory>

using namespace std;

class Father;
class Son;

class Father{
public:
    weak_ptr<Son> m_son;

    Father() {
        cout << __func__ << endl;
    };
    ~Father() {
        cout << __func__ << endl;
    };
};

class Son{
public:
    weak_ptr<Father> m_father;

    Son() {
        cout << __func__ << endl;
    };
    ~Son() {
        cout << __func__ << endl;
    };
};

int main(){
    shared_ptr<Father> father(new Father);
    shared_ptr<Son> son(new Son);

    father->m_son = son;
    son->m_father = father;

    cout << "father count " << father.use_count() << endl;
    cout << "son count " << son.use_count() << endl;
}

输出是:

1
2
3
4
5
6
Father
Son
father count 1
son count 1
~Son
~Father

总结

  1. unique_ptr通过限制用户行为实现了内存的RAII;
  2. shared_ptr通过引用计数实现了内存的RAII, 但是存在循环引用问题;
  3. shared_ptr通过扩展weak_ptr解决了循环引用的问题, 将weak_ptr当做是内存的缓存/快照.

还能总结一些方法:

  1. 设计一个工具类的时候, 不仅仅可以考虑其方法函数, 也可以在构造函数上做文章;
  2. 资源可以有访问接口和管理接口, 类比shared_ptr的资源, 资源会给_M_ptr用于访问, 也会给_M_refcount用于管理, 是分开的;