C++ std::enable_shared_from_this
1. shared_from_this
在解释std::enable_shared_from_this
之前,先看一个std::shared_ptr
典型用法:
1
2
3
4
5
6
7
#include <memory>
int main()
{
std::shared_ptr<int> pt1{ new int{ 10 } };
auto pt2{ pt1 };
}
这时pt1
和pt2
共用了引用计数,当pt1
和pt2
的生命周期都结束时,new int{10}
分配的内存会被释放。下面的做法会导致内存多次释放,因为它们没有使用共同的引用计数:
1
2
3
4
5
6
7
8
#include <memory>
int main()
{
auto pt{ new int{ 10 } };
std::shared_ptr<int> pt1{ pt };
std::shared_ptr<int> pt2{ pt };
}
从一个原始指针实例化多个 shared_ptr
是一种严重后果的编程失误。在可能的情况下,尽量使用 std::make_shared
(或 std::allocate_shared
)来减少发生此错误的可能性。
当然,我想应该也没有人这么使用std::shared_ptr
。不过下面这个错误倒是比较常见:
1
2
3
4
5
6
7
8
struct SomeData;
void SomeAPI(const std::shared_ptr<SomeData>& d) {}
struct SomeData {
void NeedCallSomeAPI() {
// 需要用this调用SomeAPI
}
};
上面这段代码需要在NeedCallSomeAPI
函数中调用SomeAPI
,而SomeAPI
需要的是一个std::shared_ptr<SomeData>
的实参。这个时候应该怎么做?
1
2
3
4
5
struct SomeData {
void NeedCallSomeAPI() {
SomeAPI(std::shared_ptr<SomeData>{this});
}
};
上面的做法是错误的,因为SomeAPI
调用结束后std::shared_ptr<SomeData>
对象的引用计数会降为0,导致this
被意外释放。
这种情况下,我们需要使用std::enable_shared_from_this
,使用方法很简单,只需要让SomeData
继承std::enable_shared_from_this<SomeData>
,然后调用shared_from_this
吗,例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <memory>
struct SomeData;
void SomeAPI(const std::shared_ptr<SomeData>& d) {}
struct SomeData:std::enable_shared_from_this<SomeData> {
static std::shared_ptr<SomeData> Create() {
return std::shared_ptr<SomeData>(new SomeData);
}
void NeedCallSomeAPI() {
SomeAPI(shared_from_this());
}
private:
SomeData() {}
};
int main()
{
auto d{ SomeData::Create() };
d->NeedCallSomeAPI();
}
2. 原理
std::enable_shared_from_this
的实现比较复杂,但是实现原理则比较简单。它内部使用了std::weak_ptr
来帮助完成指针相关控制数据的同步,而这份数据是在创建std::shared_ptr
的时候完成的。我们来重点解析这一点。
1
2
3
4
5
6
7
8
9
10
11
12
13
template<class T>
class enable_shared_from_this {
mutable weak_ptr<T> weak_this;
public:
shared_ptr<T> shared_from_this() {
return shared_ptr<T>(weak_this);
}
shared_ptr<const T> shared_from_this() const {
return shared_ptr<const T>(weak_this);
}
...
template <class U> friend class shared_ptr;
};
以上是摘要的enable_shared_from_this
的代码,这份代码中有两个关键要素。首先weak_this
被声明为mutable
,这让weak_this
可以在const
的限定下修改,其次也是最关键的地方,该类声明了shared_ptr
为友元。这意味着std::shared_ptr
可以修改weak_this
,并且weak_this
被初始化的地方在std::shared_ptr
中。进一步说,没有std::shared_ptr
的enable_shared_from_this
是没有灵魂的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <memory>
struct SomeData;
void SomeAPI(const std::shared_ptr<SomeData>& d) {}
struct SomeData:std::enable_shared_from_this<SomeData> {
void NeedCallSomeAPI() {
SomeAPI(shared_from_this());
}
};
int main()
{
auto d{ new SomeData };
d->NeedCallSomeAPI();
}
在这份代码中调用shared_from_this
会出错。
再深入一步,std::shared_ptr
是如何判断实例化对象类型是否继承std::enable_shared_from_this
,并且通过判断结果决定是否初始化weak_this
的呢?答案是SFINAE(“Substitution Failure Is Not An Error“)。
让我们查看VS2019的STL代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
template <class _Ty>
class enable_shared_from_this {
public:
using _Esft_type = enable_shared_from_this;
...
}
template <class _Yty, class = void>
struct _Can_enable_shared : false_type {};
template <class _Yty>
struct _Can_enable_shared<_Yty, void_t<typename _Yty::_Esft_type>>
: is_convertible<remove_cv_t<_Yty>*, typename _Yty::_Esft_type*>::type {
};
这里的重点是_Can_enable_shared
,如果目标类型有内嵌类型_Esft_type
,并且目标类型和内嵌类型的指针是可转换的,也就是有继承关系,那么类型结果为true_type
,反之为false_type
。
1
2
3
4
5
6
7
8
9
10
template <class _Ux>
void _Set_ptr_rep_and_enable_shared(_Ux* const _Px, _Ref_count_base* const _Rx) noexcept {
this->_Ptr = _Px;
this->_Rep = _Rx;
if constexpr (conjunction_v<negation<is_array<_Ty>>, negation<is_volatile<_Ux>>, _Can_enable_shared<_Ux>>) {
if (_Px && _Px->_Wptr.expired()) {
_Px->_Wptr = shared_ptr<remove_cv_t<_Ux>>(*this, const_cast<remove_cv_t<_Ux>*>(_Px));
}
}
}
接下来,如果对象不是数组、不是volatile
声明的并且_Can_enable_shared
返回true_type
,那么_Wptr
才会被初始化。std::shared_ptr
的构造函数以及std::make_shared
函数都会调用该函数。
以上就是std::enable_shared_from_this
实现原理中比较关键的一个部分。
3. 总结
在调用enable_shared_from_this类的shared_from_this()方法之前,必须要先初始化函数内部weak_ptr对象,否则该函数无法返回一个有效的shared_ptr对象,std::shared_ptr
的构造函数以及std::make_shared
函数都会初始化函数内部weak_ptr对象。