关于 shared_ptr

shared_ptr是一种共享所有权的智能指针,它允许我们安全地访问和管理对象的生命周期。shared_ptr的多个实例通过共享控制块结构来控制对象的生命周期。 控制块维护了引用计数(reference count),弱引用计数(weak count)和其他必要的信息,通过这些信息,控制块能够确定一个对象在内存中是否可以被安全销毁。

当使用原始指针构造或者初始化一个shared_ptr时,将会创建一个新的控制块。为了确保一个对象仅由一个共享的控制块管理,必须通过复制已存在的shared_ptr对象来创建一个新的shared_ptr实例,例如:

1void good()
2{
3  auto p{new int(10)}; // p is int*
4  // create additional shared_ptr from an existing shared_ptr
5  std::shared_ptr<int> sp1{p};
6  // sp2 shares control block with sp1
7  auto sp2{sp1};
8}

而使用指向已由shared_ptr管理的对象的原始指针来初始化另一个shared_ptr时,会创建一个新的控制块来管理该对象, 这样同一个对象就同时被多个控制块管理,这会导致 undefined behavior,例如:

1void bad()
2{
3  auto p{new int(10);};
4  std::shared_ptr<int> sp1{p};
5  std::shared_ptr<int> sp2{p}; // Undefined behavior!
6}

通过原始指针的方式实例化shared_ptr很容易产生同一个原始指针实例化多个shared_ptr这样的编码疏忽,从而造成严重后果。 因此尽量使用std::make_shared或者std::allocate_shared来降低出错的可能性。毕竟除非有人刻意为之,否则我们似乎很难遇到或写出这样的代码:

1auto sp1 = std::make_shared<int>();
2std::shared_ptr<int> sp2{sp1.get()};

但是在某些情况下,shared_ptr管理的对象需要为自己获取shared_ptr,我会在后面的篇幅中重点讲解这种情况。 但是首先需要说明的是,类似于下面这样尝试从自身指针创建shared_ptr的方式是行不通的:

 1struct Egg
 2{
 3  std::shared_ptr<Egg> get_self_ptr()
 4  {
 5    return std::shared_ptr<Egg>(this);
 6  }
 7};
 8
 9void spam()
10{
11  auto sp1 = std::make_shared<Egg>();
12  auto sp2 = sp1->get_self_ptr(); // undefined behavior
13  // sp1 and sp2 have two different control blocks managing same Egg
14}

为了解决这个问题,我们就需要用到std::enable_shared_from_thispublic 继承std::enable_shared_from_this 的类可以通过调用shared_from_this()方法来获取自身的shared_ptr,下面是一个简单的例子:

 1struct Thing;
 2void some_api(const std::shared_ptr<Thing>& tp);
 3
 4struct Thing : public std::enable_shared_from_this<Thing>
 5{
 6  void method()
 7  {
 8    some_api(shared_from_this());
 9  }
10};
11
12void foo()
13{
14  auto sp = std::make_shared<Thing>();
15  sp->method();
16}

为什么要从 this 创建 shared_ptr

让我们来看一个更有说服力的例子,在这面的例子中,shared_ptr管理的对象需要为自己获取一个shared_ptr

一个Processor类异步处理数据并将其保存到数据库。在接收数据时,Processor通过自定义执行器来异步处理数据:

 1class Executor
 2{
 3public:
 4  void execute(const std::function<void(void)>& task);
 5
 6private:
 7  // ...
 8};
 9
10class Processor
11{
12public:
13  void process_data(const std::string& data);
14
15private:
16  void do_process_and_save(const std::string& data) {
17    // process data
18    // sava data to DB
19  }
20
21private:
22  Executor* executor_;
23};

Processor类从一个Client类接收数据,这个Client持有该Processor的一个shared_ptr实例:

 1class Client
 2{
 3public:
 4  void some_method()
 5  {
 6    processor_->process_data("xxxxxx");
 7  }
 8
 9private:
10  std::shared_ptr<Processor> processor_;
11}

Executor是一个线程池,它封装了多个线程和一个任务队列,并从队列中执行不同的 task。

Processor::process_data中,我们需要将执行过程包装成 task 传递给Executor。在 task 中调用私有方法do_process_and_save, 该方法在将数据保存到数据库之前对数据进行处理。因此,构造 task 的时候,需要捕获对Processor对象本身的引用:

1void Processor::process_data(const std::string& data)
2{
3  executor_->execute([this, data]() {
4    // ...
5    do_process_and_save(data);
6  });
7}

但是,Client可以出于各种原因随时将shared_ptr丢弃或者重置为其他关联的Processor,这可能会破坏原来的Processor。 因此在执行排队的 lambda 之前或期间,捕获的this指针可能会失效。

我们可以通过在 lambda 中捕获 this 对象的 shared_ptr 来避免上面的 undefined behavior 的发生。 只要排队的 lambda 持有一个Processorshared_ptrProcessor就会保持正常的运行状态。然而,我们知道,单纯的像这样创建一个shared_ptr<Processor>(this)是行不通的。 我们需要一种机制,让一个shared_ptr管理对象以某种方式控制它的控制块,从而获取另一个自身的shared_ptr对象。而使用std::enable_shared_from_this就是为了达到了这个目的:

 1class Processor : public std::enable_shared_from_this
 2{
 3  // ...
 4};
 5
 6void Processor::process_data(const std::string& data)
 7{
 8  executor_->execute([self = shared_from_this(), data]() {
 9    // ...
10    self->do_process_and_save(data);
11  });
12}

为什么要使用 enable_shared_from_this

本质上,为一个已经被shared_ptr管理的指针对象创建额外的shared_ptr实例只能通过从可以访问控制块的 handle 生成。 该 handle 可以是一个shared_ptr,也可以是一个weak_ptr。如果一个对象有这个 handle,那么它就可以为自己创建额外的shared_ptr。 但是shared_ptr 是一个强引用,会影响受管理对象的生命周期。将shared_ptr保存到自身对象将会导致自身永远无法被释放,从而发生内存泄漏:

1struct Immortal
2{
3  std::shared_ptr<Immortal> self;
4};

解决这个问题可以通过weak_ptr来实现。weak_ptr是一种弱引用,它不会影响受管理对象的生命周期,但是在需要时可以用来获取强引用。 如果一个对象持有自身的weak_ptr,那么在需要的时候,就可以获取自身的shared_ptr:

 1class Naive
 2{
 3public:
 4  static std::shared_ptr<Naive> create()
 5  {
 6    auto sp = std::shared_ptr<Naive>(new Naive);
 7    sp->weak_self_ = sp;
 8    return sp;
 9  }
10
11  auto async_method()
12  {
13    return std::async(std::launch::async, [self = weak_self_.lock()]() {
14      self->do_something();
15    });
16  }
17
18  void do_something()
19  {
20    std::this_thread::sleep_for(std::chrono::seconds(1));
21  }
22
23private:
24  Naive() {}
25  Naive(const Naive&) = delete;
26  const Naive& operator=(const Naive&) = delete;
27  std::weak_ptr<Naive> weak_self_;
28};
29
30void test()
31{
32  std::future<void> ft;
33  {
34    auto pn = Naive::create();
35    ft = pn->async_method();
36  }
37  ft.get();
38}

上面的实现不够完美,因为它有很多限制。我们需要确保在构造Naive类的时候初始化对自身的weak_ptr,因此Naive的构造必须仅通过静态工厂方法来进行约束。 这种解决方案对合理性需求施加了太多的限制,然而这个实现却为标准解决方案std::enable_shared_from_this创建了一个概念框架。

std::enable_shared_from_this 的内部实现

std::enable_shared_from_this的典型实现是一个只包含了weak_ptr<T>字段的类,通常这个字段叫做weak_this(clang 中为__weak_this_,gcc 中为_M_weak_this):

 1template<class T>
 2class enable_shared_from_this
 3{
 4  mutable weak_ptr<T> weak_this;
 5
 6public:
 7  shared_ptr<T> shared_from_this()
 8  {
 9    return shared_ptr<T>(weak_this);
10  }
11
12  // const overload
13  shared_ptr<const T> shared_from_this() const
14  {
15    return shared_ptr<const T>(weak_this);
16  }
17
18  // .. more methods and constructors..
19  // there is weak_from_this() also since c++17
20
21  template <class U> friend class shared_ptr;
22};

std::enable_shared_from_this的核心代码就这么简单,而剩下的魔法代码在shared_ptr的构造函数中实现。 当shared_ptrT*初始化的时候,如果T是从std::enable_shared_from_this<T> 继承来的,则shared_ptr构造函数会初始化weak_this。 只有当Tstd::enable_shared_from_this 公开 继承的时候,才能在编译时使用 trait 类(std::enable_ifstd::is_convertible)来启用weak_this的初始化代码。 因此,必须使用public继承std::enable_shared_from_this类,因为shared_ptr构造函数需要通过weak_this来进行初始化,如果不 public 继承,则会在运行时抛出bad_weak_ptr异常。

关于std::enable_shared_from_this还有一些值得注意的细节:

  1. weak_ptr被声明为 mutable,因此它也可以被修改为 const 对象
  2. shared_ptr被声明为友元类型,这样它就可以访问私有字段weak_this

下面是另一个简单的例子,用来描述这一切是如何联系在一起的。我们将初始化shared_ptr的代码故意分为两个步骤,以演示嵌入的weak_ptr的创建和初始化的两个阶段:

 1struct Article : public std::enable_shared_from_this<Article>
 2{
 3};
 4
 5void foo()
 6{
 7  // step 1
 8  // Enclosed 'weak_this' is not associated with any control block.
 9  auto pa = new Article;
10
11  // step 2
12  // Enclosed 'weak_this' gets initialized with a control block
13  auto spa = std::shared_ptr<Article>(pa);
14}

关于 std::bad_weak_ptr 异常

调用shared_from_this方法有一个限制,就是只能在shared_ptr管理的对象内部调用。 从 c++17 开始,在不受shared_ptr管理的对象内部调用shared_from_this会触发std::bad_weak_ptr异常,而在 c++17 之前,这种操作是 undefined behavior。

 1struct Article : public std::enable_shared_from_this<Article>
 2{
 3  void foo()
 4  {
 5    auto self = shared_from_this();
 6    // ...
 7  }
 8}
 9
10void test()
11{
12  auto pa = new Article;
13  pa->foo(); // ! std::bad_weak_ptr
14}

shared_from_this调用的时候,如果weak_ptr未初始化或者已过期,那么shared_ptr的构造函数就会抛出异常。

另一个触发抛出std::bad_weak_ptr异常的情况是,当在构造函数中调用shared_from_this的时候,因为当前对象还没构造完,嵌入的weak_this尚未初始化而导致抛出异常。

触发std::bad_weak_ptr异常的另一个非常微妙而且难以定位排查的情况是,一个类没有使用public继承std::enable_shared_from_this类并且调用了shared_from_this方法。 privateprotected 继承都会阻止weak_this成员的初始化,这可能会在没有任何编译器警告的情况下被忽略。例如:

1class Overlooked : std::enable_shared_from_this<Overlooked>
2{
3public:
4  void foo()
5  {
6    // std::bad_weak_ptr
7    auto self = shared_from_this();
8  }
9}

有些环境下会禁用或者避免抛出异常,对于这种情况,从 c++17 开始,就有了一种替代shared_from_this的方法。 c++17 将weak_from_this方法添加到std::enable_shared_from_this中,后者返回嵌入的weak_this的副本。 shared_ptr可以安全地从该weak_ptr中获取,而不会导致任何std::bad_weak_ptr异常的发生:

 1class Overlooked : std::enable_shared_from_this<Overlooked>
 2{
 3public:
 4  void foo()
 5  {
 6    if (auto self = weak_from_this().lock()) {
 7      // ok, use self
 8    } else {
 9      // ...
10    }
11  }
12}