std::enable_shared_from_this源码原理剖析
std::enable_shared_from_this源码原理剖析
声明:之前已经踩过一次坑了,也稍微了解了一下boost的实现机制,这次又踩了一次(主要是惯性),于是去翻了下gcc9的实现,思路相同,但是实现手法不一样,遂记录一下
源码版本
我查看的是我的Ubuntu自带的版本:gcc9.3
源码位置在/usr/include/c++/9
,如果是使用Linux系统的话,应该差不太多。
通过memory
可以知道关键代码在的头文件是bits
目录下的shared_ptr.h
和shared_ptr_base.h
。
问题提出
1 | class XXX : std::enable_shared_from_this<XXX> { |
std::enable_shared_from_this<>
我想平时使用C++编程的不会对此陌生,如果你想在成员函数中传递std::shared_ptr
会被它非侵入式的设计被刺,即裸指针构造会创造新的控制块(Control block
,不了解的可以参考Scotto Meyers
的Modern Effective C++
),因此得继承该类并使用其成员函数shared_from_this()
。
这里的使用姿势是错误,不管什么时候,都应该通过std::shared_ptr
的构造函数去创建堆上的对象(当然,std::make_shared()
也可以),其他创建对象的方式均会抛出std::bad_weak_ptr
异常。
熟悉模板的读者不难看出这个用到了CRTP
手法,是因为它的内部实现确实需要知道它的子类类型。下面的源码分析可以知道其巧妙之处。
源码分析
可以看到实际上它是使用std::enable_shared_from_this
的std::weak_ptr
成员构造std::shared_ptr
。
因此想要了解哪里出了问题,就必须看下shared_ptr
的的构造函数:
看注释,我们已经知道问题出在哪了,因为weak_ptr
成员并没有指向一个合理的控制块,没有引用计数,因此它会抛出异常。
继续深挖的话,最终可以找到抛出异常的代码:
那么std::enable_shared_from_this
是如何解决这个问题的?
首先明确为什么要用到继承,这里主要是利用了多态但是没有用到虚机制,至于为什么要用到派生类到基类的转换,最后揭露。
该类正确使用方式是调用std::shared_ptr
的构造函数(或std::make_shared()
,这样就不会抛出异常。可想而知,我们得继续查看std::shared_ptr
的构造函数:
这个函数(高亮的部分)看来是关键。
熟悉模板的对std::enable_if
等<type_traits>
的设施应该很熟悉。可见它准备了一个谓词来判断该类型是否为std::enable_shared_from_this
的派生类。这里我不展开std::enable_if
的实现原理(SFINAE
),简单来说,它的作用是排除不符合的重载模板函数从而实现基于类型的重载,可见如果是基类为多做一些处理,而不是就啥都不做。
这个谓词当然也是通过SFINAE
实现的:
那么__enable_shared_from_this_base()
是个怎样的函数呢?
它是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()
不太清楚了:
它是__weak_ptr
的私有函数,而weak_ptr
是__weak_ptr
的派生类,而enable_shared_from_this
是__weak_ptr
的友元类(这里注意为什么不是weak_ptr
的友元类,因为友元不能继承)。
这个函数只有当此时引用计数为0时才进行赋值,避免了重复错误的赋值。
至此,用CRTP
的原因已经很明确了:它是启动SFINAE
的关键。这样友元函数才会实例化,从而判断出该类是enable_shared_from_this
的派生类。用到多态是通过模板实参推断出来的派生类不能通过ADL找到该友元函数的。