从本期,就开始智能指针源码分析之路,从源码中了解他们的设计。
智能指针,可以分为两类:
独占型:如std::unique_ptr
,一份资源,仅能由一个std::unique_ptr
对象管理;
共享型:如std::shared_ptr
,一份资源,可以由多个std::shared_ptr
对象共同管理,当没有std::shared_ptr
对象指向这份的资源,资源才会被释放,即基于引用技术原理。
本期,先来讲解std::unique_ptr
,之后会分几期来讲解std::shared_ptr
的设计。
不过,在讲解std::unique_ptr
之前,先讲解下C++03中的失败品:std::auto_ptr
。
std::auto_ptr 原本也是想要将 std::auto_ptr
设计成资源独占型的指针,即像现在的std::unique_ptr
,但由于移动语义直到C++11中才出现,使得std::auto_ptr
终究成了失败品。
不明白,没关系,go on。
在C++03标准下,有如下demo中的一个场景。
1 2 3 4 5 6 7 int main (int argc, char const *argv[]) { std::auto_ptr<int > iptr (new int (1 )) ; std::vector<std::auto_ptr<int > > integer_vec; integer_vec.push_back (iptr); return 0 ; }
由于在C++03标准中还没有引入移动语义,只能以push_back
函数向vector
中添加元素。
如果你没接触过std::auto_ptr
,应该会认为上面的demo是能编译通过的,但实际上是无法编译通过的。
编译指令如下:
1 $ g++ -std=c++03 main.cc -o main && ./main
下面只贴出最初的错误信息:
1 2 3 gcc-8.2 /include/c++/8.2 .0 /ext/new_allocator.h:146 :9 : error: no matching function for call to ‘std::auto_ptr<int >::auto_ptr (const std::auto_ptr<int >&)’ { ::new ((void *)__p) _Tp(__val); } ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~
先给出导致错误的结论:是由于类std::auto_ptr
没有提供const std::auto_ptr<T>&
类型的复制构造函数。
那为啥会没有呢?
因为std::auto_ptr
的设计者,想使std::auto_ptr
的复制构造函数具备移动构造函数的属性(如果不懂右值、移动等内容,可以看看 右值引用的正确用法 ),这就使得std::auto_ptr
复制构造函数的输入参数__a
不能由 const 修饰,否则__a
指向的资源就无法移动到新创建的对象中了。
1 auto_ptr (auto_ptr& __a) throw () : _M_ptr(__a.release ()) {}
这就导致std::auto_ptr
中,所有和赋值有关的操作,都不能有const
修饰。
std::auto_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 template <typename _Tp>class auto_ptr { private : _Tp *_M_ptr; public : typedef _Tp element_type; explicit auto_ptr (element_type *__p = 0 ) throw () : _M_ptr(__p) { } auto_ptr (auto_ptr& __a) throw () : _M_ptr(__a.release ()) {} template <typename _Tp1> auto_ptr (auto_ptr<_Tp1>& __a) throw () : _M_ptr(__a.release ()) {} auto_ptr &operator =(auto_ptr& __a) throw () { reset (__a.release ()); return *this ; } ~auto_ptr () { delete _M_ptr; } element_type* release () throw () { element_type *__tmp = _M_ptr; _M_ptr = 0 ; return __tmp; } void reset (element_type *__p = 0 ) throw () { if (__p != _M_ptr) { delete _M_ptr; _M_ptr = __p; } } };
by the way 对于最上面的demo,我们再啰嗦几点:为什么会在 construct
函数中报错?
因为push_back
函数中,输入参数__x
是const std::auto_ptr&
类型,能接受iptr
:
1 2 template <typename _Tp, typename _Alloc> void std::vector<_Tp, _Alloc>::push_back (const value_type& __x);
在push_back
函数内部会调用 _Alloc_traits::construct
函数来构造一个新的std::auto_ptr
对象obj
,然后将这个obj
放到integer_vec
中。
1 _Alloc_traits::construct (this ->_M_impl, this ->_M_impl._M_finish, __x);
因为要构造obj
,那么必要会调用std::auto_ptr
的复制构造函数,且输入参数是__x
;
但由于__x
是 const std::auto_ptr&
类型,二std::auto_ptr
的复制构造函数输入类型是std::auto_ptr&
,接受不了__x
作为输入,因此会导致construct
函数执行失败。出现上述的错误。
std::unique_ptr C++11引入移动语义,提出了std::unique_ptr
,才真正地完成了std::auto_ptr
的设计意图,而原本的std::auto_ptr
也被标记为deprecated
。
由于 std::unique_ptr
对象管理的资源,不可共享,只能在 std::unique_ptr
对象之间转移,因此类std::unique_ptr
就禁止了复制构造函数、赋值表达式,仅实现了移动构造函数等。
此外,std::unique_ptr
有两个版本:
管理单个对象(例如以 new
分配)
管理动态分配的对象数组(例如以 new[]
分配)
因此, std::unique_ptr
的类模板有如下两个。
1 2 3 4 5 6 7 template <typename _Tp, typename _Dp = default_delete<_Tp>>class unique_ptr { };template <typename _Tp, typename _Dp>class unique_ptr <_Tp[], _Dp> { };
在下面的情况中,std::unique_ptr
对象管理的资源,会调用传入的析构器_Dp
来释放放资源:
当前std::unique_ptr
对象被销毁,生命周期结束;
重新给当前std::unique_ptr
对象赋值,比如调用 operator=
、reset()
等操作。
下面就先讲解下默认的析构器std::default_delete
。
std::default_delete 由于std::unique_ptr
有两个版本,因此默认的析构器也存在两个版本,即对new[]
进行特化。
此外,这就导致后文的make_unique
函数也需要对new[]
进行特化。
1 2 3 4 5 template <typename _Tp>struct default_delete { }; template <typename _Tp>struct default_delete <_Tp[]> { };
由于默认采用new
、new[]
来分配内存的,而sd::default_delete
实际上是个仿函数,内部也是基于delete
、delete[]
来释放内存资源的。
delete 类std::default_delete
实际上是个仿函数,并且是个空类,因此他的默认构造函数直接设置为default
了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 template <typename _Tp>struct default_delete { constexpr default_delete () noexcept = default ; template <typename _Up, typename = typename enable_if<is_convertible<_Up*, _Tp*>::value>::type> default_delete (const default_delete<_Up>& ) noexcept { } void operator ()(_Tp *__ptr) const ; };
此外,std::default_delete
还有个复制构造函数,这里传入_Up*
参数 必须能转换为 _Tp*
类型,否则在编译期会报错。所谓_Up*
能转换为 _Tp*
,即is_convertible<_Up*, _Tp*>::value
为 true,这个值在编译期就能确定,如果为false,就相当于不存在这个复制构造函数。
不信,可以把下面的代码复制到IDE中,会有红线提示,编译会出错。
1 2 std::is_convertible<float *, double *>::value; std::default_delete<double > de (std::default_delete<float >{}) ;
在std::default_delete
的内部,实现的operator()
函数会调用delete
来 析构传入的指针__ptr
,对于__ptr
需要满足两点:
__ptr
不能是个void
类型;
大小也不能是0。
否则无法提供完整的信息去析构__ptr
指向的内存。
1 2 3 4 5 6 void operator () (_Tp *__ptr) const { static_assert (!is_void<_Tp>::value, "can't delete pointer to incomplete type" ); static_assert (sizeof (_Tp) > 0 , "can't delete pointer to incomplete type" ); delete __ptr; }
那么,你就可以这样来使用std::default_delete
:
1 2 3 int * iptr = new int ;std::default_delete <int >()(iptr);
使用valgrind
检测也不存在内存泄露。
delete[] 当输入类型是_Tp[]
时,会进入此版本。实现和上面的版本差不多。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 template <typename _Tp>struct default_delete <_Tp[]>{ public : constexpr default_delete () noexcept = default ; template <typename _Up, typename = typename enable_if<is_convertible<_Up (*)[], _Tp (*)[]>::value>::type> default_delete (const default_delete<_Up[]> &) noexcept {} template <typename _Up> typename enable_if<is_convertible<_Up (*)[], _Tp (*)[]>::value>::type operator ()(_Up *__ptr) const { static_assert (sizeof (_Tp) > 0 , "can't delete pointer to incomplete type" ); delete [] __ptr; } };
因此,这样就可以使用
1 2 3 int * iptr = new int [3 ];std::default_delete <int []>()(iptr);
使用valgrind
检查也没有任何内存泄露。
因此,std::default_delete
对两种常用的内存释放方式进行重载,提供了同一个统一接口。
std::__uniq_ptr_impl 类std::unique_ptr
内部只有一个成员变量,其类型是 std::__uniq_ptr_impl
。
类std::__uniq_ptr_impl
实际上就一个<pointer, Deleter>
的一个wrapper,即简单封装了指向资源的指针,以及对应的析构器 Deleter
。
先大致看下std::__uniq_ptr_impl
的部分实现。
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 template <typename _Tp, typename _Dp>class __uniq_ptr_impl { public : using _DeleterConstraint = enable_if<__and_<__not_<is_pointer<_Dp>>, is_default_constructible<_Dp>>::value>; __uniq_ptr_impl() = default ; __uniq_ptr_impl(pointer __p) : _M_t() { _M_ptr() = __p; } template <typename _Del> __uniq_ptr_impl(pointer __p, _Del&& __d) : _M_t(__p, std::forward<_Del>(__d)) { } pointer& _M_ptr() { return std::get <0 >(_M_t); } pointer _M_ptr() const { return std::get <0 >(_M_t); } _Dp& _M_deleter() { return std::get <1 >(_M_t); } const _Dp& _M_deleter() const { return std::get <1 >(_M_t); } void swap (__uniq_ptr_impl& __rhs) noexcept { std::swap (this ->_M_ptr(), __rhs._M_ptr()); std::swap (this ->_M_deleter(), __rhs._M_deleter()); } private : tuple<pointer, _Dp> _M_t; };
类std::__uniq_ptr_impl
的成员变量_M_t
是由tuple
类型:
第一个成员:是指针,指向资源;
第二个成员:是析构器,用于释放指针指向的资源。
由于在X86-84位系统上指针大小是8,而_Dp
在默认情况下(即std::default_delete
)是个空类,得益于空基类优化 ,因此_M_t
的大小是8。
NOTICE :如果自定义了一个Deleter
,且不是空类,则std::unique_ptr
的大小会增加。
下面,来分析下std::__uniq_ptr_impl
中的 pointer
类型,他的完整实现及注释如下。
因此,当_Dp
是默认的析构器std::default_delete
时,pointer
即 _Tp*
。
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 <typename _Tp, typename _Dp> class __uniq_ptr_impl { template <typename _Up, typename _Ep, typename = void > struct _Ptr { using type = _Up*; }; template <typename _Up, typename _Ep> struct _Ptr <_Up, _Ep, __void_t <typename remove_reference<_Ep>::type::pointer>> { using type = typename remove_reference<_Ep>::type::pointer; }; public : using pointer = typename _Ptr<_Tp, _Dp>::type; };
std::unique_ptr 分析的进度终于到了类std::unique_ptr
,它的设计主要如下四点:
禁止复制构造函数、复制赋值的重载,即设置为=delete
;
实现各种移动构造函数;
实现移动赋值重载,即operator=
,需要先释放本身的资源,再将对方的资源移动过来;
如果资源没有释放过,则会在析构函数中释放。
为便于理解,先看下std::unique_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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 template <typename _Tp, typename _Dp = default_delete<_Tp>>class unique_ptr{ template <typename _Up> using _DeleterConstraint = typename __uniq_ptr_impl<_Tp, _Up>::_DeleterConstraint::type; __uniq_ptr_impl<_Tp, _Dp> _M_t; public : using pointer = typename __uniq_ptr_impl<_Tp, _Dp>::pointer; using element_type = _Tp; using deleter_type = _Dp; private : template <typename _Up, typename _Ep> using __safe_conversion_up = __and_<is_convertible<typename unique_ptr<_Up, _Ep>::pointer, pointer>, __not_<is_array<_Up>>>; public : 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& operator =(nullptr_t ) noexcept { reset (); return *this ; } unique_ptr (const unique_ptr &) = delete ; unique_ptr &operator =(const unique_ptr &) = delete ; ~unique_ptr () noexcept { static_assert (__is_invocable<deleter_type&, pointer>::value, "unique_ptr's deleter must be invocable with a pointer" ); auto & __ptr = _M_t._M_ptr(); if (__ptr != nullptr ) get_deleter ()(std::move (__ptr)); __ptr = pointer (); } pointer get () const noexcept { return _M_t._M_ptr(); } deleter_type& get_deleter () noexcept { return _M_t._M_deleter(); } explicit operator bool () const noexcept { return get () == pointer () ? false : true ; } pointer release () noexcept { pointer __p = get (); _M_t._M_ptr() = pointer (); return __p; } void reset (pointer __p = pointer()) noexcept { static_assert (__is_invocable<deleter_type &, pointer>::value, "unique_ptr's deleter must be invocable with a pointer" ); std::swap (_M_t._M_ptr(), __p); if (__p != pointer ()) get_deleter ()(std::move (__p)); } void swap (unique_ptr &__u) noexcept { static_assert (__is_swappable<_Dp>::value, "deleter must be swappable" ); _M_t.swap (__u._M_t); } };
std::unique_ptr
的设计总体比较清晰。
到此,std::unique_ptr
的设计分析差不多就结束了,下面对源码中(上面未列出)几个模板稍微分析下。
_DeleterConstraint _DeleterConstraint
定义于std::__uniq_ptr_impl
之中,用于限制传入的析构器Deleter
必须满足以下两点:
传入的Deleter
不能是指针;
必须具备默认构造函数。
否则,std::unique_ptr
的构造函数会失败。
1 2 3 4 5 6 7 8 9 10 11 12 13 using _DeleterConstraint = enable_if<__and_<__not_<is_pointer<_Dp>>, is_default_constructible<_Dp>>::value>; template <typename _Del = _Dp, typename = _DeleterConstraint<_Del>>constexpr unique_ptr () noexcept : _M_t() { } template <typename _Del = _Dp, typename = _DeleterConstraint<_Del>>explicit unique_ptr (pointer __p) noexcept : _M_t(__p) { }
_Require 其原型如下:
1 2 template <typename ... _Cond>using _Require = __enable_if_t <__and_<_Cond...>::value>;
_Require
模板是要求所有传入的条件Cond
都为true,则_Require
修饰的函数才会存在。
1 2 3 4 5 6 7 8 9 10 11 12 template <typename _Del = deleter_type, typename = _Require<is_copy_constructible<_Del>>> unique_ptr (pointer __p, const deleter_type& __d) noexcept : _M_t(__p, __d) {} template <typename _Del = deleter_type, typename = _Require<is_move_constructible<_Del>>> unique_ptr (pointer __p, __enable_if_t <!is_lvalue_reference<_Del>::value, _Del&&> __d) noexcept : _M_t(__p, std::move (__d)) { }
__safe_conversion_up 在构造函数中,有时候需要通过其他std::unique_ptr
对象来构造当前std::unique_ptr
对象,但是pointer
类型可能不同,模板__safe_conversion_up
可以在编译期确定是否可以转换。
1 2 3 template <typename _Up, typename _Ep>using __safe_conversion_up = __and_<is_convertible<typename unique_ptr<_Up, _Ep>::pointer, pointer>, __not_<is_array<_Up>>>;
此外还有个条件模板conditional
,实现编译期的条件表达式:
1 2 3 4 5 6 7 8 9 10 11 template <bool _Cond, typename _Iftrue, typename _Iffalse>struct conditional { typedef _Iftrue type; }; template <typename _Iftrue, typename _Iffalse>struct conditional <false , _Iftrue, _Iffalse>{ typedef _Iffalse type; };
最终,可以用于构造函数。
1 2 3 4 5 6 7 8 9 10 template <typename _Up, typename _Ep, typename = _Require<__safe_conversion_up<_Up, _Ep>, typename conditional<is_reference<_Dp>::value, is_same<_Ep, _Dp>, is_convertible<_Ep, _Dp>>::type>> unique_ptr (unique_ptr<_Up, _Ep>&& __u) noexcept : _M_t(__u.release (), std::forward<_Ep>(__u.get_deleter ())) { }
std::make_unique 最后,再来看下std::make_unique
函数,它在C++14中引入,这在之前的’编译器优化之copy elision’一期中也讲解过怎么设计它。
现在,我们再来看看C++14中 std::make_unique()
的实现,如下。
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 template <typename _Tp>struct _MakeUniq { typedef unique_ptr<_Tp> __single_object; }; template <typename _Tp>struct _MakeUniq <_Tp[]>{ typedef unique_ptr<_Tp[]> __array; }; template <typename _Tp, size_t _Bound>struct _MakeUniq <_Tp[_Bound]>{ struct __invalid_type { }; }; template <typename _Tp, typename ... _Args>inline typename _MakeUniq<_Tp>::__single_object make_unique (_Args&& ...__args) { return unique_ptr <_Tp>(new _Tp(std::forward<_Args>(__args)...)); }template <typename _Tp>inline typename _MakeUniq<_Tp>::__array make_unique (size_t __num) { return unique_ptr <_Tp>(new remove_extent_t <_Tp>[__num]()); }template <typename _Tp, typename ... _Args>inline typename _MakeUniq<_Tp>::__invalid_type make_unique (_Args&& ...) = delete ;
结束,再回顾下std::unique_ptr
和 std::auto_ptr
。
可以发现std::auto_ptr
的失败在于CXX03中并不支持移动语义,而std::auto_ptr
却试图用复制构造函数来实现移动构造函数的功能,结果导致其无法与vector
等容器兼容,论为失败品。
std::unique_ptr
的分析为止。