浅谈智能指针

2023/02/24 cpp 共 16685 字,约 48 分钟

写在前面

一共有unique_ptrshared_ptrweak_ptr3种智能指针。其中unique_ptr独占有对象,shared_ptr共享对象,通过引用计数实现无引用时自动释放,weak_ptr提出是为了解决shared_ptr存在的循环计数以及生命周期模糊等问题。

unique_ptr && make_unique

std::unique_ptr占有并管理另一对象,并在 unique_ptr 离开作用域时释放该对象的智能指针,不允许复制,只能转移

std::unique_ptr<A> a1(new A());
std::unique_ptr<A> a2 = a1;//编译报错,不允许复制
std::unique_ptr<A> a3 = std::move(a1);//可以转移所有权,所有权转移后a1不再拥有任何指针

在使用unique_ptr,要稍微注意一下以下2点

  • 只有非 const 的 unique_ptr 能转移把管理对象的所有权给另一个 unique_ptr 。即若对象的生存期为 const std::unique_ptr 所管理,则它被限定在创建指针的作用域中。因为在转移的时候调用了非const成员函数。 (原因是因为赋值需要调用非const成员函数)

    const std::unique_ptr a4 = make_unique<A>();
    auto a5 = std::move(a4) // 错误,const不能调用非const成员函数
    
  • std::unique_ptr 可为不完整类型 T 构造,例如用于改善用作 pImpl 手法中柄的用途。若使用默认删除器,则 T 必须在代码中调用删除器点处完整,这发生于析构函数、移动赋值运算符和 std::unique_ptrreset 成员函数中。

make_unique简单封装了一下new,对于大多数情况,等价于unique_ptr<T>(new T)

shared_ptr && make_shared

shared_ptrunique_ptr相比多了一个引用计数,并由于引用计数的加入,可以支持拷贝。多个 shared_ptr 对象可占有同一对象。当引用计数为0的时候,在堆上delete该对象。

make_shared在堆上一起构造函数对象和引用计数的控制块,返回shared_ptr指针。

weak_ptr

用来表达临时所有权的概念:对于生命周期模糊的对象,可以使用 std::weak_ptr 来跟踪该对象,需要获得临时所有权时,调用成员函数lock,如果对象存在,将其转换为 std::shared_ptr,增加引用计数,如果对象已经被析构,则失败。

std::weak_ptr 的另一用法是打断 std::shared_ptr 所管理的对象组成的环状引用。若这种环被孤立(例如无指向环中的外部共享指针),则 shared_ptr 引用计数无法抵达零,而内存被泄露。能令环中的指针之一为弱指针以避免此情况。e.g.

class B;
class A {
 public:
    shared_ptr<B> b_;
};
class B {
 public:
    shared_ptr<A> a_;
}
// 考虑如下情况,A先离开作用域,即A引用计数减少1,然后B离开作用域,B应用计数也减少1,
// 由于sharedA、sharedB的引用计数都还有一个在互相手里,导致不会释放内存,内存泄漏
// 这时候我们需要把其中一个声明为weak_ptr,在需要用到的时候可以进行lock升级为shared_ptr
{
    auto sharedA = make_shared<A>;
    auto sharedB = make_shared<B>;
   	//引用计数都为2
    sharedA->b_ = sharedB;
    sharedA->a_ = sharedA;
}

由上弱指针用来解决智能引用的循环引用问题和类的生命周期的不稳定。

shared_from_this

在多线程编程的时候,有时候对象的生命期是非常模糊的,比如一个tcp连接,我们并不知道什么时候这个连接会断开。因此我们需要用shared_ptr代替this指针,但是传递给智能指针的时候按照下面的方式并不会增加引用计数。

#include <memory>
#include <iostream>

using namespace std;

class Widget;
void workthread(const shared_ptr<Widget>& ptr)  // 模拟工作线程
{ cout << "工作线程的计数:" << ptr.use_count() << endl; }
class Widget {
 public:
    Widget() {}
    ~Widget(){}
    void runInAnotherThread() { 
        shared_ptr<Widget> self(this); // 类内传递智能指针
        workthread(self);
    }
};
int main() {
    shared_ptr<Widget> wptr = make_shared<Widget>();
    cout << "主线程的计数:" << wptr.use_count() << endl;
    wptr->runInAnotherThread(); 
}
double free or corruption (out)
主线程的计数:1
工作线程的计数:1

在一些平台报错double free,并且在各自线程的引用计数都是1,这就意味着会被多次释放。因此需要用到shared_from_this

若一个类 T 公有继承 std::enable_shared_from_this<T> ,则会为该类 T 提供成员函数: shared_from_this ,这种方式从成员函数得到智能指针就会增加引用计数,更改后代码如下。

#include <memory>
#include <iostream>

using namespace std;

class Widget;
void workthread(const shared_ptr<Widget>& ptr)  // 模拟工作线程
{ cout << "工作线程的计数:" << ptr.use_count() << endl; }
class Widget : public enable_shared_from_this<Widget> { // modify here
 public:
    Widget() {}
    ~Widget(){}
    void runInAnotherThread() { 
        workthread(shared_from_this()); // modify here
    }
};
int main() {
    shared_ptr<Widget> wptr = make_shared<Widget>();
    cout << "主线程的计数:" << wptr.use_count() << endl;
    wptr->runInAnotherThread(); 
}

输出如下,可以得知引用计数正常增加了。

主线程的计数:1
工作线程的计数:2

如何选择智能指针

  • 由于unique_ptr不存在引用计数,因此性能较好,但是只能独占对象资源
  • shared_ptr共享对象的所有权,性能略差
  • weak_ptr配合shared_ptr,解决生命期以及循环引用等问题

简单来说,优先选择unique_ptr

Why make_shared

​ 当我们想构造一个std::shared_ptr智能指针管理对象A的时候,可能有如下代码

std::shared_ptr<A> ptr = new A(); // 形式1
std::shared_ptr<A> ptr = make_shared<A>(); // 形式2

​ 对于unique_ptrmake_unique来说,这种类似的形式是完全等价的。但对于这种情况,为了效率,我们应该优先使用make_shared,因为在这种情况下,我们把参数和类型一起传递给标准库,标准库会选择特定的shared_ptr构造函数,把智能指针引用计数需要的控制块和对象A的内存一起申请,提高了效率。

这里有性能比较Tech-Foo: Experimenting with C++ std::make_shared

img

​ 如果是形式1,由于在构造智能指针之前我们已经new出了对象,因此它还需要单独分配控制块的内存,意味着一共调用new 2次,如下所示

img

make_shared也有缺点

  • 需要构造函数是public的,不然无法从外部访问,但stackoverflow总有你想要的How do I call ::std::make_shared on a class with only protected or private constructors?
  • 由于是一起分配的,回收的时候也需要一起回收,但weak_ptr 会保持控制块(强引用, 以及弱引用的信息)的生命周期, 因此只有最后一个 weak_ptr 离开作用域时, 内存才会被释放。在使用make_shared分配对象内存的时候,当强引用计数变为0的时候,只会调用析构函数而不会返还内存。意外的延迟了内存释放的时间。
  • make_shared不能自定义删除器和分配器

源码分析(Why?)

代码来源于/usr/include/c++/9/bits目录下面,gcc9.4.0,只分析比较关键的部分。

unique_ptr

首先是声明,unique_ptr有两个模板参数,分别为_Tp_Dp

  • _Tp表示此 unique_ptr 所管理的对象类型
  • _Dp则表示析构器,可以自定义。默认delete函数。

函数声明中 pointer 可以简单理解为 _Tp*,拥有一个成员变量__uniq_ptr_impl__uniq_ptr_impl只有一个成员变量tuple<pointer, _Dp> _M_t;

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;
  // ...
}

原生指针的构造函数,用原生指针初始化__M_t_,

template <typename _Del = _Dp, typename = _DeleterConstraint<_Del>>
explicit unique_ptr(pointer __p) noexcept : _M_t(__p) {}

移动构造函数,从右值中得到指针和析构器去初始化变量,

unique_ptr(unique_ptr&& __u) noexcept
    : _M_t(__u.release(), std::forward<deleter_type>(__u.get_deleter())) {}

析构函数,如果指针不为空,调用析构器析构

~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();
}

releasereset,其中release表示交出所有权,reset表示用形参中的指针替代目前存储的指针,并且把原来的指针用析构器析构了。

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");
  using std::swap;
  swap(_M_t._M_ptr(), __p);
  if (__p != pointer()) get_deleter()(std::move(__p));
}

这里就可以知道第一个结论的原因,为什么const不能转移,因为移动构造函数或者移动赋值函数为会调用非const成员函数,const类型的实例是不能调用非const成员函数的,complie error。

下面是默认析构器的实现,基本就是直接delete

template <typename _Tp>
struct default_delete {
   // ...
  void operator()(_Tp* __ptr) const {
    static_assert(!is_void<_Tp>::value,
                  "can't delete pointer to incomplete type");
    // 这里用sizeof 计算了_Tp的大小,即这里需要保证_Tp完整
    static_assert(sizeof(_Tp) > 0, "can't delete pointer to incomplete type");
    delete __ptr;
  }
};

第二个注解的原因就在这里,这里用sizeof 计算了_Tp的大小,即这里需要保证_Tp完整,即当语义分析到这里的时候,保证了在这之前Tp类型是完整的。为什么需要这样?看下面的代码,因此 T 必须在代码中调用删除器点处完整。

class A;
A* a = ...; // 只有前置声明 不是完整的类型
delete a; // Undefined behavior 

由于这个特性的存在,有时候会出现意想不到的问题,代码如下,在使用桥接模式用智能指针替代裸指针的时候,很自然就写成下面的形式,但会出现编译错误:incomplete error

class foo
{ 
class impl; // define else
std::unique_ptr<impl> impl_; // incomplete error
public:
foo(); 
~foo() = default;
};
// 导致编译失败
{
 foo a; // point 1
} 

编译器会认为impl是不完整的,这时候需要把~foo() = default;放到cpp文件中,因为如果我们放在头文件中,析构函数会内联到 比如point 1 后面,而foo的析构函数会调用unique_ptr的析构函数(同样也是内联),但在这时候并不是一个完整的类,因此需要把析构函数~foo() = default;放到cpp文件中。移动复制函数、移动赋值函数等也需要这样放到相应的cpp文件中,详细可以参考《Effective Modern C++》条款22。

make_unique

这里只展示了匹配非数组类型的,前面的std::enable_if_t<!std::is_array<T>::value, std::unique_ptr<T>>为了判断是否为数组类型,匹配非数组类型,

template <class T, class... Args>
std::enable_if_t<!std::is_array<T>::value, std::unique_ptr<T>> 
	make_unique(Args&&... args) {
  return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

shared_ptr

shared_ptr由于引用计数机制的存在,比unique_ptr复杂不少,这里精简介绍

shared_ptr从基类__shared_ptr中继承而来,拥有友元类weak_ptr;基类 __shared_ptr__shared_ptr_access派生而来,_Constructible,利用模板匹配(SFINAE)用来判断参数类型是否可以用来构造__shared_ptr。赋值函数底层调用基类的赋值函数。

template <typename _Tp>
class shared_ptr : public __shared_ptr<_Tp> {
  //是否可以构造__shared_ptr
  using _Constructible = typename enable_if<
      is_constructible<__shared_ptr<_Tp>, _Args...>::value>::type;
  //其中一个构造函数
  template <typename _Yp, typename = _Constructible<_Yp*>>
  explicit shared_ptr(_Yp* __p) : __shared_ptr<_Tp>(__p) {}
  //其中一个赋值构造函数,接收一个左值引用
  template <typename _Yp>
  _Assignable<const shared_ptr<_Yp>&> operator=(
      const shared_ptr<_Yp>& __r) noexcept {
    this->__shared_ptr<_Tp>::operator=(__r);
    return *this;
  }

  friend class weak_ptr<_Tp>;
};

其中主要实现依靠基类__shared_ptr,在构造函数中需要检查类型是否完整,因此和unique_ptr类似的,shared_ptr要求在调用构造函数的点有完整的类型。shared_ptr从类 __shared_ptr_access继承而来,从名字就知道,这个基类是负责帮派生类处理operator*operator->

// 主要的实现类
template <typename _Tp, _Lock_policy _Lp>
class __shared_ptr : public __shared_ptr_access<_Tp, _Lp> {
  //构造函数
  template <typename _Yp, typename = _SafeConv<_Yp>>
  explicit __shared_ptr(_Yp* __p)
      : _M_ptr(__p), _M_refcount(__p, typename is_array<_Tp>::type()) {
    static_assert(!is_void<_Yp>::value, "incomplete type");
    static_assert(sizeof(_Yp) > 0, "incomplete type"); // 需要检查是否完整
    _M_enable_shared_from_this_with(__p);
  }
  // 上面派生类会调用左值引用的赋值函数
  template <typename _Yp>
  _Assignable<_Yp> operator=(const __shared_ptr<_Yp, _Lp>& __r) noexcept {
    _M_ptr = __r._M_ptr;
    _M_refcount = __r._M_refcount;  // __shared_count::op= doesn't throw
    return *this;
  }
  // 析构函数是默认的,
  ~__shared_ptr() = default;

 private:
  element_type* _M_ptr;             // Contained pointer.
  __shared_count<_Lp> _M_refcount;  // Reference counter.
}

__shared_count负责引用计数,主要观察赋值函数确实会调用 _M_add_ref_copy,而这个函数是原子增加的。引用计数是线程安全的。其中真正的引用计数存放的位置在_M_pi指针指向的地方,而析构函数会减少引用计数。

template <__gnu_cxx::_Lock_policy _Lp>
class std::__shared_count<_Lp> {
  // 赋值函数,增加引用计数
  __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;
  }
  //析构函数,减少引用计数
  ~__shared_count() noexcept {
    if (_M_pi != nullptr)
      _M_pi->_M_release();  // 减少引用计数,如果减为0,则析构控制块内存
  }

  _Sp_counted_base<_Lp>* _M_pi;  // 真正的引用计数存放位置
} 
void _M_add_ref_copy() {
  __gnu_cxx::__atomic_add_dispatch(&_M_use_count, 1);
}

小结

shared_ptr的大部分功能实现由基类__shared_ptr完成,基类中的 __shared_count通过成员变量_Sp_counted_base完成了对引用计数的控制,

make_shared

​ 上面谈论了有关引用计数的问题,这里主要讨论内存分配的问题。首先看make_shared,利用万能引用把参数包通过完美转发给allocate_shared,并且传递了内存分配器为allocator(这意味着make_shared只能std::allocator去分配内存?),然后传递给函数allocate_shared。然后私有构造函数转给基类去调用,基类传递给了_M_refcount构造函数

template <typename _Tp, typename... _Args>
inline shared_ptr<_Tp> make_shared(_Args&&... __args) {
  typedef typename std::remove_cv<_Tp>::type _Tp_nc;
  return std::allocate_shared<_Tp>(std::allocator<_Tp_nc>(),
                                   std::forward<_Args>(__args)...);
}
// shared_ptr的友元函数
template <typename _Tp, typename _Alloc, typename... _Args>
inline shared_ptr<_Tp> allocate_shared(const _Alloc& __a, _Args&&... __args) {
  return shared_ptr<_Tp>(_Sp_alloc_shared_tag<_Alloc>{__a},
                         std::forward<_Args>(__args)...);
}

// 私有构造函数
template <typename _Alloc, typename... _Args>
shared_ptr(_Sp_alloc_shared_tag<_Alloc> __tag, _Args&&... __args)
    : __shared_ptr<_Tp>(__tag, std::forward<_Args>(__args)...) {}
// 基类构造函数
template <typename _Alloc, typename... _Args>
__shared_ptr(_Sp_alloc_shared_tag<_Alloc> __tag, _Args&&... __args)
    : _M_ptr(), _M_refcount(_M_ptr, __tag, std::forward<_Args>(__args)...) {
  _M_enable_shared_from_this_with(_M_ptr);
}
// 最后一直传递给_M_refcount的构造函数(见下)

__shared_count的相关构造函数如下,其中最重要的类是 _Sp_counted_ptr_inplace ,基类_Sp_counted_base有2个计数成员,同时 _Sp_counted_ptr_inplace 内部有一个 _M_storage,大小和管理的指针类型一样大,这样一来,make_shared分配内存的时候就是一起分配的, 按照 _Sp_cp_type 为颗粒进行内存分配,使用__alloc_rebind可以更改分配器的颗粒度。

template <typename _Tp, typename _Alloc, typename... _Args>
__shared_count(_Tp*& __p, _Sp_alloc_shared_tag<_Alloc> __a, _Args&&... __args) {
  typedef _Sp_counted_ptr_inplace<_Tp, _Alloc, _Lp> _Sp_cp_type;
  typename _Sp_cp_type::__allocator_type __a2(__a._M_a); // 得到类内分配器
  auto __guard = std::__allocate_guarded(__a2); // 按照_Sp_cp_type为颗粒进行内存分配
  _Sp_cp_type* __mem = __guard.get(); 
  auto __pi =  
      ::new (__mem) _Sp_cp_type(__a._M_a, std::forward<_Args>(__args)...);
  __guard = nullptr;
  _M_pi = __pi;  //控制块的地址,
  __p = __pi->_M_ptr(); // 管理的指针
}

// 基类有2个引用计数 + 一个管理的类的大小
template <typename _Tp, typename _Alloc, _Lock_policy _Lp>
class _Sp_counted_ptr_inplace final : public _Sp_counted_base<_Lp> {
  class _Impl : _Sp_ebo_helper<0, _Alloc> {
    typedef _Sp_ebo_helper<0, _Alloc> _A_base;

   public:
    explicit _Impl(_Alloc __a) noexcept : _A_base(__a) {}

    _Alloc& _M_alloc() noexcept { return _A_base::_S_get(*this); }

    __gnu_cxx::__aligned_buffer<_Tp> _M_storage;
  };
//  ...
 public:
  using __allocator_type = __alloc_rebind<_Alloc, _Sp_counted_ptr_inplace>;
//  ...
  _Impl _M_impl;
};

template <_Lock_policy _Lp = __default_lock_policy>
class _Sp_counted_base : public _Mutex_base<_Lp> {
// ...
 private:
  _Atomic_word _M_use_count;   // #shared
  _Atomic_word _M_weak_count;  // #weak + (#shared != 0)
};

上面,我们确实了解了make_shared就是整块内存一起分配。很奇怪?为什么没有看见定义删除器?很明显,使用make_shared不能自定义分配器和删除器。

当没有使用自定义删除器的时候,调用的计数构造函数如下,直接new分配控制块内存。其中引用计数在_Sp_counted_base中,_M_ptr指向管理的对象

template <typename _Ptr>
explicit __shared_count(_Ptr __p) : _M_pi(0) {
  __try {
    _M_pi = new _Sp_counted_ptr<_Ptr, _Lp>(__p);
  }
  __catch(...) {
    delete __p;
    __throw_exception_again;
  }
}

template <typename _Ptr, _Lock_policy _Lp>
class _Sp_counted_ptr final : public _Sp_counted_base<_Lp> {
// ...
 private:
  _Ptr _M_ptr;
};

创建内存的事情我们已经了解了,如何销毁对象呢,销毁对象肯定和引用计数块的析构函数有关,很容易找到如下代码(删除了一些多线程代码),逻辑很简单,如果强引用计数变为0,调用 _M_dispose(),然后弱引用计数为0,调用 _M_destroy

void std::_Sp_counted_base<_Lp>::_M_release() noexcept {
  if (__gnu_cxx::__exchange_and_add_dispatch(&_M_use_count, -1) == 1) { // 强引用计数变为0
    _M_dispose();

    if (_Mutex_base<_Lp>::_S_need_barriers) {
      __atomic_thread_fence(__ATOMIC_ACQ_REL);
    }

    if (__gnu_cxx::__exchange_and_add_dispatch(&_M_weak_count, -1) == 1) { // 弱引用计数为0
      _M_destroy(); 
    }
  }
}

但前面给我们的经验是make_shared是只调用析构函数而不是释放对象的资源。因为对象资源是和引用计数块一体的。我们如何选择了,这里用到了虚函数多态。在使用make_shared_ptr的情况下,会调用_Sp_counted_base的派生类 _Sp_counted_ptr_inplace中的虚函数,这个函数是不会释放内存的,只会调用析构函数

virtual void _M_dispose() noexcept {
  allocator_traits<_Alloc>::destroy(_M_impl._M_alloc(), _M_ptr());
}

而在普通情况下,会调用派生类 _Sp_counted_ptr中的析构函数,直接释放资源。

virtual void _M_dispose() noexcept
{ delete _M_ptr; }

终于粗略的谈论完了shared_ptr,设计还是比较复杂的。弱指针就不讨论了,weak_ptr也包括两个对象,一个是原生指针,一个是控制块。虽然weak_ptr内存储了原生指针,不过由于未实现operator->因此不能直接使用。在lock的时候得到提升,成为shared_ptr。讨论最后一个重要的话题, enable_shared_from_this

enable_shared_from_this

enable_shared_from_this的源码大致如下,一般作为我们要管理的类的基类,SO?如何做到类的引用计数共享的?

​ 首先,有一个友元函数 __enable_shared_from_this_base,有一个weak_ptr指针,

template <typename _Tp>
class enable_shared_from_this {
 protected:
  constexpr enable_shared_from_this() noexcept {}
  // ...
  ~enable_shared_from_this() {}

 public:
  shared_ptr<_Tp> shared_from_this() {
    return shared_ptr<_Tp>(this->_M_weak_this);
  }
// ...

 private:
  template <typename _Tp1>
  void _M_weak_assign(_Tp1* __p, const __shared_count<>& __n) const noexcept {
    _M_weak_this._M_assign(__p, __n);
  }
  // ...
  friend const enable_shared_from_this* __enable_shared_from_this_base(
      const __shared_count<>&, const enable_shared_from_this* __p) {
    return __p;
  }
  // ...
  mutable weak_ptr<_Tp> _M_weak_this;
};

​ 关键的点在下面的函数中,首先第一个using语句。declvaldecltype 表达式中不必经过构造函数就能使用成员函数。通过下面这个句子就可以判断出是否从 enable_shared_from_this继承而来,

template <typename _Yp>
using __esft_base_t = decltype(__enable_shared_from_this_base(
    std::declval<const __shared_count<_Lp>&>(), std::declval<_Yp*>()));
 
template <typename _Yp, typename = void>
struct __has_esft_base : false_type {};

template <typename _Yp>
struct __has_esft_base<_Yp, __void_t<__esft_base_t<_Yp>>>
    : __not_<is_array<_Tp>> {};  // No enable shared_from_this for arrays

​ 然后在 __shared_ptr的构造函数中,会调用 _M_enable_shared_from_this_with,把对象指针传给模板函数 _M_enable_shared_from_this_with ,之后把类型传递给 __has_esft_base,先匹配偏特化也就是第二个版本,如果 __esft_base_t<_Yp>有类型,即using那一串有类型,即满足2个条件

  • 能找到 __enable_shared_from_this_base
  • 函数__enable_shared_from_this_base的参数与 std::declval<const __shared_count<_Lp>&>(),std::declval<_Yp*>()匹配

首先第一个要求如果 _Yp* 会根据ADL找到private友元函数,第二个要求,第一个参数已经是指针满足要求,第二个参数为enable_shared_from_this指针或者派生类指针就可以成功!

即,如果是派生类就会成功匹配友元函数,就可以推导出返回值,可以传给 __void_t(只要能推出类型,什么都可以传给它),匹配第二个模板函数,之后进入判断 __not_<is_array<_Tp>>,如果为true_type__has_esft_base<_Yp2>::valuetrue_type,有type,匹配下面的模板3,成功赋值给 _M_weak_assign,这样就统一起来了,只要是派生 enable_shared_from_this以及不是数组类型就会每次创建shared_ptr的时候统一执行函数 _M_weak_assign,完成赋值。

template <typename _Yp, typename = _SafeConv<_Yp>>
explicit __shared_ptr(_Yp* __p)
    : _M_ptr(__p), _M_refcount(__p, typename is_array<_Tp>::type()) {
  static_assert(!is_void<_Yp>::value, "incomplete type");
  static_assert(sizeof(_Yp) > 0, "incomplete type");
  _M_enable_shared_from_this_with(__p);
}

// 模板3
template <typename _Yp, typename _Yp2 = typename remove_cv<_Yp>::type>
typename enable_if<__has_esft_base<_Yp2>::value>::type
_M_enable_shared_from_this_with(_Yp* __p) noexcept {
  if (auto __base = __enable_shared_from_this_base(_M_refcount, __p))
    __base->_M_weak_assign(const_cast<_Yp2*>(__p), _M_refcount);
}

template <typename _Yp, typename _Yp2 = typename remove_cv<_Yp>::type>
typename enable_if<!__has_esft_base<_Yp2>::value>::type
_M_enable_shared_from_this_with(_Yp*) noexcept {}

总结enable_shared_from_this的原理,首先每一个从enable_shared_from_this继承的类,必须在调用内部的调用了shared_from_this()函数之前,使用shared_ptr管理,然后shared_ptr在初始化的时候,会调用一个函数根据萃取把控制块传递给该类的基类,即 enable_shared_from_this类,完成基类的赋值,之后调用的时候直接调用 shared_from_this 即可用基类的weak_ptr初始化为 shared_ptr

reference

std::unique_ptr - cppreference.com

Make_shared, almost a silver bullet

C++ 智能指针最佳实践&源码分析 - 知乎 (zhihu.com)

std::make_shared, std::make_shared_for_overwrite - cppreference.com

enable_shared_from_this

Search

    Table of Contents