概念说明

当一个类是被 shared_ptr 管理的时候,不是所有的操作都可以被 shared_ptr 内部的计数器识别的。shared_ptr 如果是被用普通指针,或者干脆是一个内存地址赋值的时候,两个 shared_ptr 之间是没有交流的,根本无从知道彼此的存在,所以也就无从对自己的计数器作出变更。比如如下的代码:

int *ip = new int;
shared_ptr<int> sp1(ip);
shared_ptr<int> sp2(ip);

运行完毕之后会有如下的提示

free(): double free detected in tcache 2

因为两个 shared_ptr 都从一个普通指针初始化,彼此相互是无从相互认识的,因此都认为自己是唯一管理这段内存的 shared_ptr,所以最后会对同一个内存地址释放两次,这种行为有时会造成严重的后果。

这种两个 shared_ptr 没有办法知道彼此存在的情况亦存在在使用类与对象的时候。比如如下这段代码:

struct S {
  shared_ptr<S> dangerous() {
    return shared_ptr<S>(this); // don't do this!
  }
};

int main() {
  shared_ptr<S> sp1(new S);
  shared_ptr<S> sp2 = sp1->dangerous();
  return 0;
}

看似解决了,因为在类是被 shared_ptr 管理着的,而返回的值也是使用 shared_ptr 在类的内部指向了 this,这样两个 shared_ptr 应该相互认识了吧?但是很可惜,普通的类是不支持这样做的,它是没有办法为这样的两个 shared_ptr 牵线搭桥的,调用 dangerous()函数的时候,它仍然相当于从一个普通指针(本来就是啊)初始化。

这样会导致的问题是:本来返回 shared_ptr 的目的可能是需要之后管理,而且保证这段 new 出来的内存不被释放(new 出来的内存是动态管理的,释放需要手动释放,而智能指针会在计数器为 0 的时候自动释放这段 new 出来的内存)。

但是当这个 Class 自身是被 shared_ptr 管理的时候,这个 shared_ptr (例如上面的 sp1)可能就会在它的生命周期过后被释放掉了,如果像 asio 的例程里面的那种直接 make_shared 而不为其赋值的操作,当执行完这个分号,这个 shared_ptr 可能就已经被销毁了,如果使用上面这种方法,计数器减一,为零,智能指针就会释放内存,从而导致我们 return 出来的那个 shared_ptr 指向的内存已经被释放了,我们不仅面临对同一段内存释放两次的风险,而且还面临着对一段空内存地址进行操作的风险。

那要怎么样才可以呢?那么就是要想办法让它支持就好了嘛。enable_shared_from_this就是解决方法,它是一个基类,可以用它来创造支持这种特性的类。这个类有一个函数,叫作 shared_from_this() -> shared_ptr<T> ,它返回的 shared_ptr 会避免这种问题。

struct S : enable_shared_from_this<S> {
  shared_ptr<S> not_dangerous() { return shared_from_this(); }
};

int main() {
  shared_ptr<S> sp1(new S);
  shared_ptr<S> sp2 = sp1->not_dangerous();
  return 0;
}

何时使用?

在异步编程中,当使用如下的方法的时候:

std::make_shared<session>(std::move(socket))->start();

start() 函数中也包含有异步进程的时候,这句语句就会执行完了,然后,这个生成的 shared_ptr 就会释放这段内存。因此在 start() 函数中注册的异步进程回调的时候,就会发现它想要调用的资源已经被释放掉了。

所以在定义这个 session 类的时候,需要使用 enable_shared_from_this这个基类,并且在注册回调函数的时候捕获 self,如下:

  void do_write() {
    std::time_t now = std::time(0);
    shared_const_buffer buffer(std::ctime(&now));

    auto self(shared_from_this());
    asio::async_write(socket_, buffer,
                      [self](std::error_code /*ec*/, std::size_t /*length*/) {});
  }

这样,self 的生命周期就会持续到回调函数结束才会被释放,所以就达成了目的。

一些注意事项

enable_shared_from_this 原理如下:

Code:

class Person : public std::enable_shared_from_this<Person> {
   //...
};

Explain:

The problem is that shared_ptr stores itself in a private member of Person’s base class, enable_shared_from_this<>, at the end of the construction of the Person.

来源网址

shared_from_this() 不能在构造函数里面使用它,因为作为我们定义的类的基类,它会在构造函数结束的时候将指向它自己的 shared_ptr 储存起来,但是在构造函数里面没有办法做到初始化自己的 shared_ptr.