源码分析auto_ptr & unique_ptr 设计
从本期,就开始智能指针源码分析之路,从源码中了解他们的设计。
智能指针,可以分为两类:
- 独占型:如
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中的一个场景。
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是能编译通过的,但实际上是无法编译通过的。
编译指令如下:
$ g++ -std=c++03 main.cc -o main && ./main
下面只贴出最初的错误信息:
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
指向的资源就无法移动到新创建的对象中了。
auto_ptr(auto_ptr& __a) throw() : _M_ptr(__a.release()) {}
这就导致std::auto_ptr
中,所有和赋值有关的操作,都不能有const
修饰。
std::auto_ptr
的核心代码如下。
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
: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
中。_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
的类模板有如下两个。
/// 适合 new 分配的内存 template <typename _Tp, typename _Dp = default_delete<_Tp>> class unique_ptr { /****/ }; /// 针对 new[] 特化 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[]
进行特化。
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
了。
template <typename _Tp> struct default_delete { /// @brief 默认构造函数 constexpr default_delete() noexcept = default; /// @brief 复制构造函数 /// _Up* 必须能转为 _Tp*,否则无法编译通过 template <typename _Up, typename = typename enable_if<is_convertible<_Up*, _Tp*>::value>::type> default_delete(const default_delete<_Up>& ) noexcept { } /// @brief 释放内存 void operator()(_Tp *__ptr) const; };
此外,std::default_delete
还有个复制构造函数,这里传入_Up*
参数 必须能转换为 _Tp*
类型,否则在编译期会报错。所谓_Up*
能转换为 _Tp*
,即is_convertible<_Up*, _Tp*>::value
为 true,这个值在编译期就能确定,如果为false,就相当于不存在这个复制构造函数。
不信,可以把下面的代码复制到IDE中,会有红线提示,编译会出错。
std::is_convertible<float*, double*>::value; // false: float* 不能直接转换为 double* std::default_delete<double> de(std::default_delete<float>{}); // compile error
在std::default_delete
的内部,实现的operator()
函数会调用delete
来 析构传入的指针__ptr
,对于__ptr
需要满足两点:
__ptr
不能是个void
类型;- 大小也不能是0。
否则无法提供完整的信息去析构__ptr
指向的内存。
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
:
int* iptr = new int; //... std::default_delete<int>()(iptr); // delete iptr;
使用valgrind
检测也不存在内存泄露。
delete[]
当输入类型是_Tp[]
时,会进入此版本。实现和上面的版本差不多。
template <typename _Tp> struct default_delete<_Tp[]> { public: /// @brief 默认构造函数 constexpr default_delete() noexcept = default; /// @brief 复制构造函数 template <typename _Up, typename = typename enable_if<is_convertible<_Up (*)[], _Tp (*)[]>::value>::type> default_delete(const default_delete<_Up[]> &) noexcept {} /// @brief 释放内存 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; } };
因此,这样就可以使用
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
的部分实现。
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; // pointer 的定义后文讲解 };
类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*
。
template <typename _Tp, typename _Dp> class __uniq_ptr_impl { /// 原型 /// @brief 特化版本决议失败,则会进入此版本 /// @type 此时 type 就是 _up* template <typename _Up, typename _Ep, typename = void> struct _Ptr { using type = _Up*; }; /// 特化版本 /// @brief 如果 类 _Ep 中有 pointer 的定义,则会进入此特化版本 /// @type 此时 type 是 _Ep 中的 pointer 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
的部分源码及其注释,另一个特化版本差不多。
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: /// 用于检测从另一个 std::unique_ptr 对象转换过来是否安全 template <typename _Up, typename _Ep> using __safe_conversion_up = __and_<is_convertible<typename unique_ptr<_Up, _Ep>::pointer, pointer>, __not_<is_array<_Up>>>; public: /*** Move constructors. ***/ /// @brief 移动构造函数 unique_ptr(unique_ptr&& __u) noexcept : _M_t(__u.release(), std::forward<deleter_type>(__u.get_deleter())) { } /// @brief 移动赋值 unique_ptr& operator=(unique_ptr&& __u) noexcept { reset(__u.release()); get_deleter() = std::forward<deleter_type>(__u.get_deleter()); return *this; } /// @brief 设置 unique_ptr 对象为初始化状态 unique_ptr& operator=(nullptr_t) noexcept { reset(); return *this; } /// @brief 禁止复制 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(); // 设置为初始化状态 } /// @brief 返回指针 pointer get() const noexcept { return _M_t._M_ptr(); } /// @brief 返回一个指向内部的deleter的引用 deleter_type& get_deleter() noexcept { return _M_t._M_deleter(); } /// Return @c true if the stored pointer is not null. explicit operator bool() const noexcept { return get() == pointer() ? false : true; } /// Release ownership of any stored pointer. 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
的构造函数会失败。
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
其原型如下:
template<typename... _Cond> using _Require = __enable_if_t<__and_<_Cond...>::value>;
_Require
模板是要求所有传入的条件Cond
都为true,则_Require
修饰的函数才会存在。
/// @brief 如果传入的 _Del 没有复制构造函数,则unique_ptr此版本构造函数就不存在 template <typename _Del = deleter_type, typename = _Require<is_copy_constructible<_Del>>> unique_ptr(pointer __p, const deleter_type& __d) noexcept : _M_t(__p, __d) {} /// @brief 如果传入的 _Del 没有移动构造函数,则 unique_ptr此版本构造函数就不存在 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
可以在编译期确定是否可以转换。
template <typename _Up, typename _Ep> using __safe_conversion_up = __and_<is_convertible<typename unique_ptr<_Up, _Ep>::pointer, pointer>, __not_<is_array<_Up>>>;
此外还有个条件模板conditional
,实现编译期的条件表达式:
// _Cond ? _Iftrue : _Iffalse; // 原型 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; };
最终,可以用于构造函数。
/// @brief 由其他std::unique_ptr对象 __u 构造this 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()
的实现,如下。
/*** 返回类型 ***/ /// 原型 template <typename _Tp> struct _MakeUniq { typedef unique_ptr<_Tp> __single_object; }; /// @brief 为数组类型特化 template <typename _Tp> struct _MakeUniq<_Tp[]> { typedef unique_ptr<_Tp[]> __array; }; /// @brief 无效 template <typename _Tp, size_t _Bound> struct _MakeUniq<_Tp[_Bound]> { struct __invalid_type { }; }; /*** 函数实现 ***/ /// std::make_unique for single objects 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)...)); } /// std::make_unique for arrays of unknown bound template <typename _Tp> inline typename _MakeUniq<_Tp>::__array make_unique(size_t __num) { return unique_ptr<_Tp>(new remove_extent_t<_Tp>[__num]()); } /// Disable std::make_unique for arrays of known bound 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
的分析为止。