std::enable_shared_from_this():诞生和作用


概述

std::shared_from_this()是 C++11 标准引入的功能,但之前一直都没用到过,直到在boost中使用asio需要异步保存回调函数时为了保存io_context,防止提前析构才了解到这个。

作用

简单地说就是帮助你怎么在class内部拿到this的shared_ptr版本。

class A {
  public:
    void func() {
      std::shared_ptr<A> local_sp_a(this);
      // do something with local_sp_a
    }
}

int main() {
  A* a;
  std::shared_ptr<A> sp_a(a);
  a->func();
  // sp_a becomes dangling.
}

通过上述方式拿的话,表面上看起来你是拿到了一个this的shared_ptr版本,但是由于计数器和被管理的对象是分离的,因此相当于2个计数器(reference count都=1),同时只有一个被管理的对象。函数内部的那个计数器,在函数调用完成之后,认为reference count变成0了,于是释放掉了对应的object。main里构造的计数器当然不知道对应的object已经被释放了,于是就会二重释放宕掉,换句话说:智能共享指针不能够直接从this对象进行构造。

C++解决方案是通过继承一个类(该方法是侵入式的,也可以手动保证只有构造第一个指向被持有对象的shared_ptr实例是由原始指针构造的),这个类本质上会给被管理的object上加一个指向计数器的weak ptr,于是就可以正确地增加引用计数而不是搞出2个独立的计数器。

原理

该类是一个类模版,通过模版参数TP将被持有对象的类型传递进来,然后在类的内部构件一个weak_ptr类型的成员变量。该成员变量用来保存指向自身的指针以及控制块相关信息,外部使用的时候如前一章节示例代码一样,为方便讨论代码重复贴出代码如下,采用继承的方式来扩展派生类的功能。

class Test : public std::enable_shared_from_this<Test>

上面这种继承自一个类模版,将自身类型以模版参数的形式传入派生类的方式有一个专有名词对应,那就是CRTP,英文全称curious recurring template pattern,中文译名是奇异递归模版模式。可以通过该模式将派生类的类型信息通过模版参数的方式传入enable_shared_from_this类模版中,在enable_shared_from_this类模版中,将用该类型信息声明内部的用于管理派生类对象的成员变量__weak_this,并且声明相关成员方法,从而扩展派生类的功能。

该类的方法可以通过调用基类提供的成员函数shared_from_this来获取一个指向自身的shared_ptr对象,通过weak_from_this方法来获取一个指向自身的weak_ptr对象,其中weak_ptr对象也是指向被管理对象的智能指针,只不过该智能指针是弱引用关系,不管理“被持有”对象的声明周期(这里被持有是带有引号的,用来表示区别于shared_ptr,这里是弱引用,不具有被管理对象的所有权),只提供访问对象数据的功能,但是和裸指针(raw pointer)功能类似,但是可以通过该指针的成员方法expire来判断被指向对象是否还有效(是否已经被销毁)。

核心逻辑在shared_from_this函数内部,该函数的内部逻辑不复杂,仅仅是通过成员变量__weak_this_来构建一个shared_ptr对象,然后将该对象返回:

shared_ptr<_Tp> shared_from_this(){
    return shared_ptr<_Tp>(__weak_this_);
}

enable_shared_from_this在异步求值中的应用

enable_shared_from_this真实的应用场景是什么?如果只是需要正常让shared_ptr计数的话完全有其他更简单的方法(工厂方法),之前提到的应用场景大部分是在类的外部,可以通过对已有的shared_ptr进行复制,来共享所有权。在类的内部如果想安全的实现所有权的共享,并且和外部的shared_ptr来共同的管理对象,这个时候就需要使用enable_shared_from_this模版类了。

看一下下面的例子,定义了一个Process类,该类通过SetContext设置了一些上下文信息,然后保存在类的内部,然后在内部通过lambda函数捕捉自身this指针,并且封装一个求值的操作,然后将该lambda函数交给一个线程池来进行异步的执行:

在异步执行的时候,调用它

运行上述代码,会发现运行异常,异常结果如下图所示

之所以会产生异常,是因为当异步调用开始执行的时候,Process对象实例_process已经被释放了,此时加入线程池的lambda函数对象内捕捉的this指针指向的对象已经被释放了,此时异步执行的时候,会导致未定义行为。

解决

如上图中间红框部分,在进行异步调用函数封装的时候,不直接去捕捉this指针,而通过shared_from_this函数来构造一个shared_ptr对象,来捕捉这个shared_ptr对象,从而达到异步调用中使用的shared_ptr对象和外部的(通过Create创建以及后续从已有shared_ptr对象拷贝复制的)shared_ptr对象共享所有权,共同管理被指向对象的生命周期。从而避免异步执行时,捕捉的对象被释放的问题。

需要注意的地方

像下面这样的写法是不正确的,

class MyCar:public std::enable_shared_from_this<MyCar>{
public:
  shared_ptr<MyCar> get_ptr() {
    return shared_from_this();
  }
  ~MyCar() {
    std::cout << "free ~Mycar()" << std::endl;
  }
private:

};

int main()
{
  MyCar* _myCar = new MyCar();
  shared_ptr<MyCar> _myCar1 = _myCar->get_ptr();
  shared_ptr<MyCar> _myCar2 = _myCar->get_ptr();
  std::cout << _myCar1.use_count() << std::endl;
  std::cout << _myCar2.use_count() << std::endl;
  return 0;
}

区别在于在调用时候没有对智能指针进行构造,而是想通过get直接得到(符合直觉),但是这样的写法是会报错的:

报错内容

如上图所示,异常位置是在弱指针处,弱指针实际上是智能共享指针的伴随指针,它主要负责监控智能指针的声明周期,弱指针本身的构造和析构都不会对引用计数进行修改,纯粹是作为一个助手监视shared_ptr管理的资源是否存在。因此弱指针的初始化是通过智能指针的构造函数来实现的,在上面的代码中对智能指针初始化时并没有使用构造函数的方式,因为弱指针是没有正常进行初始化的,所以抛出了异常。

这也是为什么推荐和工程模式一起使用,避免了这种问题。


文章作者: JoyTsing
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 JoyTsing !
评论
  目录