关于 shared_ptr
shared_ptr是一种共享所有权的智能指针,它允许我们安全地访问和管理对象的生命周期。shared_ptr的多个实例通过共享控制块结构来控制对象的生命周期。
控制块维护了引用计数(reference count),弱引用计数(weak count)和其他必要的信息,通过这些信息,控制块能够确定一个对象在内存中是否可以被安全销毁。
当使用原始指针构造或者初始化一个shared_ptr时,将会创建一个新的控制块。为了确保一个对象仅由一个共享的控制块管理,必须通过复制已存在的shared_ptr对象来创建一个新的shared_ptr实例,例如:
| |
而使用指向已由shared_ptr管理的对象的原始指针来初始化另一个shared_ptr时,会创建一个新的控制块来管理该对象,
这样同一个对象就同时被多个控制块管理,这会导致 undefined behavior,例如:
| |
通过原始指针的方式实例化shared_ptr很容易产生同一个原始指针实例化多个shared_ptr这样的编码疏忽,从而造成严重后果。
因此尽量使用std::make_shared或者std::allocate_shared来降低出错的可能性。毕竟除非有人刻意为之,否则我们似乎很难遇到或写出这样的代码:
| |
但是在某些情况下,shared_ptr管理的对象需要为自己获取shared_ptr,我会在后面的篇幅中重点讲解这种情况。
但是首先需要说明的是,类似于下面这样尝试从自身指针创建shared_ptr的方式是行不通的:
| |
为了解决这个问题,我们就需要用到std::enable_shared_from_this。public 继承std::enable_shared_from_this
的类可以通过调用shared_from_this()方法来获取自身的shared_ptr,下面是一个简单的例子:
| |
为什么要从 this 创建 shared_ptr
让我们来看一个更有说服力的例子,在这面的例子中,shared_ptr管理的对象需要为自己获取一个shared_ptr。
一个Processor类异步处理数据并将其保存到数据库。在接收数据时,Processor通过自定义执行器来异步处理数据:
| |
Processor类从一个Client类接收数据,这个Client持有该Processor的一个shared_ptr实例:
| |
Executor是一个线程池,它封装了多个线程和一个任务队列,并从队列中执行不同的 task。
在Processor::process_data中,我们需要将执行过程包装成 task 传递给Executor。在 task 中调用私有方法do_process_and_save,
该方法在将数据保存到数据库之前对数据进行处理。因此,构造 task 的时候,需要捕获对Processor对象本身的引用:
| |
但是,Client可以出于各种原因随时将shared_ptr丢弃或者重置为其他关联的Processor,这可能会破坏原来的Processor。
因此在执行排队的 lambda 之前或期间,捕获的this指针可能会失效。
我们可以通过在 lambda 中捕获 this 对象的 shared_ptr 来避免上面的 undefined behavior 的发生。
只要排队的 lambda 持有一个Processor的shared_ptr,Processor就会保持正常的运行状态。然而,我们知道,单纯的像这样创建一个shared_ptr<Processor>(this)是行不通的。
我们需要一种机制,让一个shared_ptr管理对象以某种方式控制它的控制块,从而获取另一个自身的shared_ptr对象。而使用std::enable_shared_from_this就是为了达到了这个目的:
| |
为什么要使用 enable_shared_from_this
本质上,为一个已经被shared_ptr管理的指针对象创建额外的shared_ptr实例只能通过从可以访问控制块的 handle 生成。
该 handle 可以是一个shared_ptr,也可以是一个weak_ptr。如果一个对象有这个 handle,那么它就可以为自己创建额外的shared_ptr。
但是shared_ptr 是一个强引用,会影响受管理对象的生命周期。将shared_ptr保存到自身对象将会导致自身永远无法被释放,从而发生内存泄漏:
| |
解决这个问题可以通过weak_ptr来实现。weak_ptr是一种弱引用,它不会影响受管理对象的生命周期,但是在需要时可以用来获取强引用。
如果一个对象持有自身的weak_ptr,那么在需要的时候,就可以获取自身的shared_ptr:
| |
上面的实现不够完美,因为它有很多限制。我们需要确保在构造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):
| |
std::enable_shared_from_this的核心代码就这么简单,而剩下的魔法代码在shared_ptr的构造函数中实现。
当shared_ptr用T*初始化的时候,如果T是从std::enable_shared_from_this<T> 继承来的,则shared_ptr构造函数会初始化weak_this。
只有当T从std::enable_shared_from_this 公开 继承的时候,才能在编译时使用 trait 类(std::enable_if和std::is_convertible)来启用weak_this的初始化代码。
因此,必须使用public继承std::enable_shared_from_this类,因为shared_ptr构造函数需要通过weak_this来进行初始化,如果不 public 继承,则会在运行时抛出bad_weak_ptr异常。
关于std::enable_shared_from_this还有一些值得注意的细节:
weak_ptr被声明为 mutable,因此它也可以被修改为 const 对象shared_ptr被声明为友元类型,这样它就可以访问私有字段weak_this
下面是另一个简单的例子,用来描述这一切是如何联系在一起的。我们将初始化shared_ptr的代码故意分为两个步骤,以演示嵌入的weak_ptr的创建和初始化的两个阶段:
| |
关于 std::bad_weak_ptr 异常
调用shared_from_this方法有一个限制,就是只能在shared_ptr管理的对象内部调用。
从 c++17 开始,在不受shared_ptr管理的对象内部调用shared_from_this会触发std::bad_weak_ptr异常,而在 c++17 之前,这种操作是 undefined behavior。
| |
当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方法。
private 和 protected 继承都会阻止weak_this成员的初始化,这可能会在没有任何编译器警告的情况下被忽略。例如:
| |
有些环境下会禁用或者避免抛出异常,对于这种情况,从 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异常的发生:
| |