libc++ 智能指针实现分析

前段时间做安装包大小分析时,为了解大量使用到的智能指针展开情况,对libc++的shared_ptr、weak_ptr及相关实现作了一些分析,这里整理相关笔记。

shared_ptr

memory layout

1
2
3
4
5
6
7
8
template<class _Tp>
class shared_ptr {
private:
element_type* __ptr_;
__shared_weak_count* __cntrl_;

struct __nat {int __for_bool_;};
};

可以看到有3个成员,包括:

  1. 被封装的原指针
  2. 保存有引用计数对象的地址
  3. 目前已经没有用到的整型成员。这里应该是历史实现有用到,为了ABI兼容而保留的。

引用计数对象抽象类:__shared_weak_count

先上经过裁剪的代码,展示对象的layout和接口。

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
class __shared_weak_count : private __shared_count {
long __shared_weak_owners_;
public:
explicit __shared_weak_count(long __refs = 0);
protected:
virtual ~__shared_weak_count();
public:
void __add_shared();
void __add_weak();
void __release_shared();
void __release_weak();
long use_count() const;
__shared_weak_count* lock();
virtual const void* __get_deleter(const type_info&) const;
private:
virtual void __on_zero_shared_weak() = 0;
};

class __shared_count {
__shared_count(const __shared_count&);
__shared_count& operator=(const __shared_count&);
protected:
long __shared_count;
virtual ~__shared_count();
private:
virtual void __on_zero_shared() = 0;
public:
explicit __shared_count(long __refs = 0);
void __add_shared();
bool __release_shared();
long use_count() const;
};

可以得到如下信息:

  1. 基类__shared_count实现了对强引用计数的相关操作;__shared_weak_count作为派生类扩展了对弱引用的支持并实现相关操作;
  2. __shared_weak_count包含两个long成员,分别为保存弱引用计数的__shared_weak_owners_和继承自__shared_count,保存强引用计数的__shared_count
  3. __shared_weak_count被实现为一个抽象类而非模板类,包含两个纯虚函数。阅读源码可知,__on_zero_shared_weak在所有所有强弱引用指针被释放后调用;__on_zero_shared在所有强引用指针被释放后调用。

引用计数相关操作

引用计数本身概念很简单,强持有(对应一个shared_ptr对象的构造)引用计数+1,取消强持有(对应一个shared_ptr对象的析构)引用计数-1。引用计数被减为0时,调用__on_zero_shared来释放所管理的对象。

为了保证线程安全,libc++采用C++11的原子操作来管理引用计数,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
void __add_shared() _NOEXCEPT {
__libcpp_atomic_refcount_increment(__shared_owners_);
}
bool __release_shared() _NOEXCEPT {
if (__libcpp_atomic_refcount_decrement(__shared_owners_) == -1) {
__on_zero_shared();
return true;
}
return false;
}
long use_count() const _NOEXCEPT {
return __libcpp_relaxed_load(&__shared_owners_) + 1;
}

__add_shared为增加引用计数的操作。__libcpp_atomic_refcount_increment被实现为relaxed内存序的原子fetch_add 1,因为这里只需要保证「计数+1」这个操作是原子的即可。

__release_shared为减少引用计数的操作。__libcpp_atomic_refcount_decrement被实现为ACQ_REL内存序的原子fetch_sub 1操作,因为除了要保证「计数-1」操作的原子性,还需要保证load操作带有aquire语义,避免后续的__on_zero_shared()被重排到操作前。

use_count为读取当前引用计数的操作。__libcpp_relaxed_load被实现为relaxed内存序的原子读操作。

以上有关C++11内存序(memory order)的介绍可阅读我的另一篇文章,多核并行基础

类似的,__shared_weak_count类对于弱引用的支持也是通过对弱引用计数执行相似的原子操作实现:

1
2
3
4
5
6
7
8
9
void __add_weak() _NOEXCEPT {
__libcpp_atomic_refcount_increment(__shared_weak_owners_);
}
void __shared_weak_count::__release_weak() noexcept {
if (__libcpp_atomic_load(&__shared_weak_owners_, _AO_Acquire) == 0) {
__on_zero_shared_weak();
} else if (__libcpp_atomic_refcount_decrement(__shared_weak_owners_) == -1)
__on_zero_shared_weak();
}

构造

通过查看shared_ptr的数据成员我们可以大致猜测,构造shared_ptr对象需要一个裸指针和一个引用计数对象。

首先来看直接使用裸指针的构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
template<class _Tp>
template<class _Yp>
shared_ptr<_Tp>::shared_ptr(_Yp* __p,
typename enable_if<__compatible_with<_Yp, element_type>::value, __nat>::type)
: __ptr_(__p)
{
unique_ptr<_Yp> __hold(__p);
typedef typename __shared_ptr_default_allocator<_Yp>::type _AllocT;
typedef __shared_ptr_pointer<_Yp*, __shared_ptr_default_delete<_Tp, _Yp>, _AllocT > _CntrlBlk;
__cntrl_ = new _CntrlBlk(__p, __shared_ptr_default_delete<_Tp, _Yp>(), _AllocT());
__hold.release();
__enable_weak_this(__p, __p);
}

梳理一下掰开来看,可以得到这些信息:

  1. 接受一个_Yp*参数。第二个参数为利用SFINAE保证_Yp*是可以转为_Tp*类型的,在构造函数声明中给了一个默认值。因此构造只需要传入_Yp*参数;
  2. new了一个引用计数对象,为__shared_ptr_pointer类型;
  3. 调用__enable_weak_this以提供对std::enable_shared_from_this的支持;
  4. std::shared_ptr的两个指针类型成员,即__ptr___cntrl_分别被原始指针和2中新分配的引用计数对象初始化。

再来看make_shared的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
template<class _Tp, class ..._Args>
make_shared(_Args&& ...__args) {
static_assert(is_constructible<_Tp, _Args...>::value, "Can't construct object in make_shared");
typedef __shared_ptr_emplace<_Tp, allocator<_Tp> > _CntrlBlk;
typedef allocator<_CntrlBlk> _A2;
typedef __allocator_destructor<_A2> _D2;

_A2 __a2;
unique_ptr<_CntrlBlk, _D2> __hold2(__a2.allocate(1), _D2(__a2, 1)); // <- (1)
::new(__hold2.get()) _CntrlBlk(__a2, _VSTD::forward<_Args>(__args)...); // <- (2)

_Tp *__ptr = __hold2.get()->get();
return shared_ptr<_Tp>::__create_with_control_block(__ptr, __hold2.release());
}

template<class _Yp, class _CntrlBlk>
static shared_ptr<_Tp>
shared_ptr<_Tp>::__create_with_control_block(_Yp* __p, _CntrlBlk* __cntrl) _NOEXCEPT {
shared_ptr<_Tp> __r;
__r.__ptr_ = __p;
__r.__cntrl_ = __cntrl;
__r.__enable_weak_this(__r.__ptr_, __r.__ptr_);
return __r;
}

我们知道make_shared的工作除了分配构造shared_ptr还有分配构造被管理对象,从上述代码可以知道与shared_ptr的裸指针构造函数主要不同在于:

  1. 引用计数对象为__shared_ptr_emplace类型;
  2. 步骤(1)先为引用计数对象和被管理对象分配内存,然后步骤(2)执行placement new来调用被管理对象的构造函数;
  3. 原始对象和引用计数对象的内存被分配在了一起,然后将其地址给到两个成员__ptr___cntrl_

引用计数对象

首先展开来看一下裸指针构造函数的__shared_ptr_pointer

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
template <class _Tp, class _Dp, class _Alloc>
class __shared_ptr_pointer : public __shared_weak_count {
__compressed_pair<__compressed_pair<_Tp, _Dp>, _Alloc> __data_;
public:
__shared_ptr_pointer(_Tp __p, _Dp __d, _Alloc __a)
: __data_(__compressed_pair<_Tp, _Dp>(__p, _VSTD::move(__d)), _VSTD::move(__a)) {}
virtual const void* __get_deleter(const type_info&) const _NOEXCEPT;
private:
virtual void __on_zero_shared() _NOEXCEPT;
virtual void __on_zero_shared_weak() _NOEXCEPT;
};

template <class _Tp, class _Dp, class _Alloc>
void __shared_ptr_pointer<_Tp, _Dp, _Alloc>::__on_zero_shared() _NOEXCEPT {
__data_.first().second()(__data_.first().first());
__data_.first().second().~_Dp();
}
template <class _Tp, class _Dp, class _Alloc>
void __shared_ptr_pointer<_Tp, _Dp, _Alloc>::__on_zero_shared_weak() _NOEXCEPT {
typedef typename __allocator_traits_rebind<_Alloc, __shared_ptr_pointer>::type _Al;
typedef allocator_traits<_Al> _ATraits;
typedef pointer_traits<typename _ATraits::pointer> _PTraits;

_Al __a(__data_.second());
__data_.second().~_Alloc();
__a.deallocate(_PTraits::pointer_to(*this), 1);
}

可以看到是上述__shared_weak_count的派生类,包含三个模板参数:指针类型、释放器类型和分配器类型。

它扩展了一个__compressed_pair类型的数据成员。与std::pair不同,如果__compressed_pair的空间占用会根根据模板参数类型大小是否为空压缩,其实现是两个成员是通过继承__compressed_pair_elem,而非像std::pair一样直接声明两个模板参数类型的成员。

__shared_ptr_pointer的实现可以知道,相对于基类,它携带了原始指针、释放器和分配器的信息,并实现了基类定义的两个接口:

  1. __on_zero_shared保证了在被管理对象的引用计数为0时,释放掉被管理对象的内存;

  2. __on_zero_shared_weak保证了在所有被管理对象的强弱引用对象(即shared_ptr和weak_ptr)都释放后,释放掉引用计数对象本身的内存。

再来看make_shared使用的__shared_ptr_emplace

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
template <class _Tp, class _Alloc>
class __shared_ptr_emplace: public __shared_weak_count {
__compressed_pair<_Alloc, _Tp> __data_;
public:
__shared_ptr_emplace(_Alloc __a): __data_(_VSTD::move(__a), __value_init_tag()) {}
template <class ..._Args>
__shared_ptr_emplace(_Alloc __a, _Args&& ...__args)
: __data_(piecewise_construct, _VSTD::forward_as_tuple(__a),
_VSTD::forward_as_tuple(_VSTD::forward<_Args>(__args)...)) {}

private:
virtual void __on_zero_shared() _NOEXCEPT;
virtual void __on_zero_shared_weak() _NOEXCEPT;
public:
_Tp* get() _NOEXCEPT {return _VSTD::addressof(__data_.second());}
};

template <class _Tp, class _Alloc>
void __shared_ptr_emplace<_Tp, _Alloc>::__on_zero_shared() _NOEXCEPT {
__data_.second().~_Tp();
}
template <class _Tp, class _Alloc>
void __shared_ptr_emplace<_Tp, _Alloc>::__on_zero_shared_weak() _NOEXCEPT {
typedef typename __allocator_traits_rebind<_Alloc, __shared_ptr_emplace>::type _Al;
typedef allocator_traits<_Al> _ATraits;
typedef pointer_traits<typename _ATraits::pointer> _PTraits;
_Al __a(__data_.first());
__data_.first().~_Alloc();
__a.deallocate(_PTraits::pointer_to(*this), 1);
}

同样是__shared_weak_count的派生类。最大不同点在于:

  1. __shared_ptr_pointer只持有被管理对象的指针,而__shared_ptr_emplace拥有被管理对象完整内存;
  2. __on_zero_shared只负责调用被管理对象析构函数,其占用内存的释放要等到__on_zero_shared_weak阶段连同引用计数对象本身一起释放

shared_ptr<T>::shared_ptr(pointer) vs. make_shared<T>(...)

从上述代码分析我们可以知道:

  1. 通过make_shared来构造智能指针,被管理对象的和引用计数对象内存被分配在一起,只执行了一次堆内存分配,且其引用计数对象相比于__shared_ptr_pointer少占用了一个指针的空间;
  2. 通过make_shared来构造智能指针,被管理对象占用内存的释放相对于直接从裸指针构造可能更晚。要等到所有指向它的weak_ptr被释放后,才跟随引用计数对象一起释放

weak_ptr

memory layout

1
2
3
4
5
6
template<class _Tp>
class weak_ptr {
private:
element_type* __ptr_;
__shared_weak_count* __cntrl_;
};

和shared_ptr类似,包含有原指针和引用计数对象的指针。

构造

以从shared_ptr作为参数的构造函数为例:

1
2
3
4
5
6
7
8
9
10
11
12
template<class _Tp>
template<class _Yp>
inline
weak_ptr<_Tp>::weak_ptr(shared_ptr<_Yp> const& __r,
typename enable_if<is_convertible<_Yp*, _Tp*>::value, __nat*>::type)
_NOEXCEPT
: __ptr_(__r.__ptr_),
__cntrl_(__r.__cntrl_)
{
if (__cntrl_)
__cntrl_->__add_weak();
}

相对于shared_ptr就很简单了,主要就是执行add_weak操作,来增加引用计数对象的__shared_weak_owners_成员计数。

lock成员函数

通过weak_ptr获取被管理对象时,我们需要使用weak_ptr<T>::lock成员函数,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<class _Tp>
shared_ptr<_Tp> weak_ptr<_Tp>::lock() const _NOEXCEPT {
shared_ptr<_Tp> __r;
__r.__cntrl_ = __cntrl_ ? __cntrl_->lock() : __cntrl_;
if (__r.__cntrl_)
__r.__ptr_ = __ptr_;
return __r;
}

__shared_weak_count* __shared_weak_count::lock() noexcept {
long object_owners = __libcpp_atomic_load(&__shared_owners_);
while (object_owners != -1)
{
if (__libcpp_atomic_compare_exchange(&__shared_owners_,
&object_owners,
object_owners+1))
return this;
}
return nullptr;
}

同样是通过原子操作来实现。lock()函数中,通过CAS操作,获取引用计数并+1,成功则返回引用计数对象的指针,并构造对应shared_ptr;如果获取到的引用计数已经是-1,被管理对象已释放,则返回空指针,构造一个空的shared_ptr返回。

这里weak_ptr是shared_ptr的友元类,因此可以直接赋值shared_ptr的成员。

shared_from_this

当我们需要在一个被智能指针管理的对象的成员函数中,返回指向自身的shared_ptr时会遇到问题。结合上述分析可以知道,因为如果使用this指针构造shared_ptr,则又会创建一个引用计数对象来管理。

因此我们需要继承类std::enable_shared_from_this<T>,通过其方法shared_from_this()去访问shared_ptr的实现细节,即__cntrl_成员,来构造一个指向自身的shared_ptr。

来看一下该类的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
template<class _Tp>
class enable_shared_from_this
{
mutable weak_ptr<_Tp> __weak_this_;
protected:
enable_shared_from_this() _NOEXCEPT {}
enable_shared_from_this(enable_shared_from_this const&) _NOEXCEPT {}
enable_shared_from_this& operator=(enable_shared_from_this const&) _NOEXCEPT
{return *this;}
~enable_shared_from_this() {}
public:
shared_ptr<_Tp> shared_from_this()
{return shared_ptr<_Tp>(__weak_this_);}
shared_ptr<_Tp const> shared_from_this() const
{return shared_ptr<const _Tp>(__weak_this_);}
template <class _Up> friend class shared_ptr;
};

有一个成员__weak_this_。如果类继承了std::enable_shared_from_this,则会拥有这个成员,即指向自身的weak_ptr,从而即可实现shared_from_this()方法,那么问题就变成了何时对该成员复制。

这就是前部分shared_ptr构造过程结束前,__enable_weak_this方法所做的事情,来看该成员函数的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
template <class _Yp, class _OrigPtr>
typename enable_if<is_convertible<_OrigPtr*,
const enable_shared_from_this<_Yp>*
>::value,
void>::type
shared_ptr<_Tp>::__enable_weak_this(const enable_shared_from_this<_Yp>* __e,
_OrigPtr* __ptr) _NOEXCEPT {
typedef typename remove_cv<_Yp>::type _RawYp;
if (__e && __e->__weak_this_.expired())
{
__e->__weak_this_ = shared_ptr<_RawYp>(*this,
const_cast<_RawYp*>(static_cast<const _Yp*>(__ptr)));
}
}

void shared_ptr<_Tp>::__enable_weak_this(...) _NOEXCEPT {}

也是借助了SFINAE,当被管理对象的类继承了std::enable_shared_from_this时,则会初始化其__weak_this_成员。

总结

综上,libc++实现的std::shared_ptr / std::weak_ptr功能主要通过它们的__cntrl__成员,即指向一个引用计数对象的指针实现:

  1. 分配:在make_shared或从std::shared_ptr的被管理对象的原始指针构造方法中首次被分配;
  2. 管理:shared_ptr的其他拷贝、移动、赋值操作,或创建对应的weak_ptr,都是在共享同一个引用计数对象的指针,对这个对象进行引用计数的增减操作;
  3. 释放:在引用计数归零后,引用计数对象负责析构被管理对象并释放内存;在所有引用被管理对象的智能指针都释放后,引用计数对象自身才被最终析构、释放。