std::enable_shared_from_this源码原理剖析

声明:之前已经踩过一次坑了,也稍微了解了一下boost的实现机制,这次又踩了一次(主要是惯性),于是去翻了下gcc9的实现,思路相同,但是实现手法不一样,遂记录一下

源码版本

我查看的是我的Ubuntu自带的版本:gcc9.3
源码位置在/usr/include/c++/9,如果是使用Linux系统的话,应该差不太多。

通过memory可以知道关键代码在的头文件是bits目录下的shared_ptr.hshared_ptr_base.h

问题提出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class XXX : std::enable_shared_from_this<XXX> {
XXX() = default;

void f()
{
//...
}
};

int main()
XXX x{};

x.f(); // Ops! Throw std::bad_weak_ptr
}

std::enable_shared_from_this<>我想平时使用C++编程的不会对此陌生,如果你想在成员函数中传递std::shared_ptr会被它非侵入式的设计被刺,即裸指针构造会创造新的控制块(Control block,不了解的可以参考Scotto MeyersModern Effective C++),因此得继承该类并使用其成员函数shared_from_this()

这里的使用姿势是错误,不管什么时候,都应该通过std::shared_ptr的构造函数去创建堆上的对象(当然,std::make_shared()也可以),其他创建对象的方式均会抛出std::bad_weak_ptr异常。

熟悉模板的读者不难看出这个用到了CRTP手法,是因为它的内部实现确实需要知道它的子类类型。下面的源码分析可以知道其巧妙之处。

源码分析

image.png
image.png
image.png
可以看到实际上它是使用std::enable_shared_from_thisstd::weak_ptr成员构造std::shared_ptr
因此想要了解哪里出了问题,就必须看下shared_ptr的的构造函数:
image.png
看注释,我们已经知道问题出在哪了,因为weak_ptr成员并没有指向一个合理的控制块,没有引用计数,因此它会抛出异常。
继续深挖的话,最终可以找到抛出异常的代码:
image.png
image.png
image.png
image.png
那么std::enable_shared_from_this是如何解决这个问题的?
首先明确为什么要用到继承,这里主要是利用了多态但是没有用到虚机制,至于为什么要用到派生类到基类的转换,最后揭露。

该类正确使用方式是调用std::shared_ptr的构造函数(或std::make_shared(),这样就不会抛出异常。可想而知,我们得继续查看std::shared_ptr的构造函数:
image.png
这个函数(高亮的部分)看来是关键。
image.png
熟悉模板的对std::enable_if<type_traits>的设施应该很熟悉。可见它准备了一个谓词来判断该类型是否为std::enable_shared_from_this的派生类。这里我不展开std::enable_if的实现原理(SFINAE),简单来说,它的作用是排除不符合的重载模板函数从而实现基于类型的重载,可见如果是基类为多做一些处理,而不是就啥都不做。
这个谓词当然也是通过SFINAE实现的:
image.png
那么__enable_shared_from_this_base()是个怎样的函数呢?
image.png
它是std::enable_shared_from_this<T>这个类模板的友元函数,我这里之所以强调友元函数,是因为类模板的非模板友元函数比较特殊,首先它没有暴露在类外,因此和非模板类的友元相同只能通过ADL(Argument dependent lookup)找到。因为它只有在被用到时才会进行实例化(On-demand instantiation),这就意味着如果你不是继承的该类,这个函数的实例是不存在的,自然__enable_shared_from_this_base()的调用是错误(error),但是SFINAE认为它不是错误而是失败(failure),所以可以忽略,因此上面的__has_esft_base会因为偏特化被排除,所以只能特化为主模板。
(如果不了解SFINAE还是建议了解一下,全称叫Substitution failure is not a error)。

最后,就是这个_M_assign()不太清楚了:
image.png
它是__weak_ptr的私有函数,而weak_ptr__weak_ptr的派生类,而enable_shared_from_this__weak_ptr的友元类(这里注意为什么不是weak_ptr的友元类,因为友元不能继承)。
这个函数只有当此时引用计数为0时才进行赋值,避免了重复错误的赋值。

至此,用CRTP的原因已经很明确了:它是启动SFINAE的关键。这样友元函数才会实例化,从而判断出该类是enable_shared_from_this的派生类。用到多态是通过模板实参推断出来的派生类不能通过ADL找到该友元函数的。