前两篇文章都关于智能指针。第一篇由Observer
模式切入,主要讨论了std::weak_ptr
的比较操作以及避免std::shared_ptr
环形引用,提及了std::enable_shared_from_this
,主要集中在应用方面。第二篇由多态切入,从MSVC
的实现剖析std::shared_ptr
内部指针所指涉的静态类型以解释析构行为。然而还有一些关键问题没有澄清,比如智能指针的内存布局。就如学 C++ 要学习Object Model
,对于标准库或工作中使用的第三方库,应努力了解工具的内存布局,才能更好地在编码设计中做取舍。
对于标准库部分,本文讨论对象仍为std::shared_ptr
,主要根据MSVC
的实现剖析四种构造方式下的内存布局。此外,基于boost::intrusive_ptr
补充侵入式智能指针的实现。更好的例子请参考llvm::IntrusiveRefCntPtr
。
- 基本构造
std::shared_ptr<T>(raw_pointer)
由于裸指针已经指涉了一块内存,没办法挨着这块内存分配Control Block
,故Object
与Control Block
分离。
- 含自定义析构器
std::shared_ptr<T>(raw_pointer, deleter)
deleter
会纳入Control Block
。对于std::shared_ptr
,析构器的类型不是该类型的组成部分。
- 使用
std::make_shared<T>(construct_parameters)
相比基本构造,由于没有预先分配内存,这种构造可以分配单块内存,将Object
与Control Block
捆绑在一起。
- 父类为
std::enable_shared_from_this<DerivedClass>
这种定义类的方式使用了 CRTP。由于标准库的std::shared_ptr
采用非侵入式设计,为了弥补引用计数与托管对象分离,导致无法在类中安全获取指涉到自身的std::shared_ptr
,库补充了std::enable_shared_from_this
组件用于侵入对象解决此问题。
boost::intrusive_ptr
侵入式设计,把Control Block
塞入Object
中共存亡,避免出现两个Control Block
管理一个Object
的情况。
展示的源码大多经过 SFINAE 筛选并实例化,内存布局会辅以图片。对模板推导的细节感兴趣可参考上一篇文章。基础知识可参考《Modern Effective C++》(条款19与21),尤其是这些构造法的适用场景。
[1] 基本构造
案例
#include <memory>
class Demo {};
int main()
{
auto pDemo = std::shared_ptr<Demo>(new Demo);
return 0;
}
分析:
template <>
class _Ptr_base<Demo> {
public:
// ...
protected:
constexpr _Ptr_base() noexcept = default;
~_Ptr_base() = default;
private:
Demo* _Ptr{ nullptr };
_Ref_count_base* _Rep{ nullptr };
};
template <>
class shared_ptr<Demo> : public _Ptr_base<Demo> {
public:
explicit shared_ptr(Demo* _Px) {
_Set_ptr_rep_and_enable_shared(_Px, new _Ref_count<_Ux>(_Px));
}
}
对于std::shared_ptr<Demo>(new Demo)
,在构造函数体执行之前,首先是new
,然后创建父类std::_Ptr_base<Demo>
,最后是子类std::shared_ptr<Demo>
。
接着是在构造函数内的实参:
new _Ref_count<_Ux>(_Px);
class _Ref_count_base {
public:
// ...
protected:
constexpr _Ref_count_base() noexcept = default;
private:
unsigned long _Uses = 1;
unsigned long _Weaks = 1;
}
template <>
class _Ref_count<Demo> : public _Ref_count_base {
public:
explicit _Ref_count(Demo* _Px) : _Ref_count_base(), _Ptr(_Px) {}
private:
Demo* _Ptr;
};
先是父类std::_Ref_count_base
,含记录std::shared_ptr
的引用计数_Uses
以及记录std::weak_ptr
的引用计数_Weaks
。子类std::_Ref_count<Demo>
还有自己的数据_Ptr
。
现在没有std::weak_ptr
,为什么将_Weaks
初始化为1?注意这里,_Uses
负责Demo Instance
的引用计数。而_Weaks
负责std::_Ref_count<Demo> Instance
的引用计数。以后的std::weak_ptr
会在此基础上增加。减为 0 时std::_Ref_count<Demo> Instance
被销毁,而这总是发生在_Uses
减少为 0 后,否则会发生内存泄漏。
void shared_ptr<Demo>::_Set_ptr_rep_and_enable_shared(Demo* const _Px, _Ref_count_base* const _Rx) noexcept {
this->_Set_ptr_rep(_Px, _Rx);
}
void _Ptr_base<Demo>::_Set_ptr_rep(Demo* _Other_ptr, _Ref_count_base* _Other_rep) noexcept {
_Ptr = _Other_ptr;
_Rep = _Other_rep;
}
这里设置数据成员。
图中有两个指针指向实例对象。std::shared_ptr
中的主要服务于各种operator
,可以方便拿到裸指针。而std::_Ref_count
负责管理实例的生命周期,在销毁时时使用。析构细节可参考上一篇文章。
简化一下,其实就是:
读过《Effective Modern C++》肯定觉得似曾相识,这是最基本的std::shared_ptr
的内存分布。所谓避免多个std::shared_ptr
管理同一个对象以防止多次释放同一内存,意思是任何时候只能有一个Control Block
来管理object
的生命周期。而这也是std::enable_shared_from_this
所解决的主要问题,如果在类内std::shared_ptr<T>(this)
这样的语句,则会创建新的Control Block
。必须继承std::enable_shared_from_this
,使用shared_from_this()
来安全地获取,这样不会创建新的Control Block
。
[2] 含自定义析构器
案例
#include <memory>
class Demo {};
int main()
{
auto pDemoWithDeleter = std::shared_ptr<Demo>(new Demo, [](Demo* p) { delete p; });
return 0;
}
分析:
template <>
class shared_ptr<Demo> : public _Ptr_base<Demo> {
shared_ptr(Demo* _Px, LambdaExpr _Dt) {
_Setpd(_Px, std::move(_Dt));
}
};
void shared_ptr<Demo>::_Setpd(const Demo* _Px, LambdaExpr _Dt)
_Set_ptr_rep_and_enable_shared(
_Px, new _Ref_count_resource<Demo*, LambdaExpr>(_Px, std::move(_Dt)));
}
template <>
class _Ref_count_resource<Demo*, LambdaExpr> : public _Ref_count_base {
public:
_Ref_count_resource(Demo* _Px, LambdaExpr _Dt)
: _Ref_count_base(), _Mypair(_One_then_variadic_args_t(), std::move(_Dt), _Px) {}
private:
_Compressed_pair<LambdaExpr, Demo*> _Mypair;
};
与普通构造的区别就是,std::_Ref_count<Demo>::_Ptr
换成了std::_Ref_count_resource<Demo*, LambdaExpr>::_Mypair
。
std::_Compressed_pair
是个很有意思的类,这里给出的代码完全保留了原模板定义,可以仔细分析:
template <class _Ty1, class _Ty2, bool = is_empty_v<_Ty1> && !is_final_v<_Ty1>>
class _Compressed_pair final : private _Ty1 { // store a pair of values, deriving from empty first
public:
_Ty2 _Myval2;
using _Mybase = _Ty1; // for visualization
template <class... _Other2>
constexpr explicit _Compressed_pair(_Zero_then_variadic_args_t, _Other2&&... _Val2)
: _Ty1(), _Myval2(_STD forward<_Other2>(_Val2)...) {}
template <class _Other1, class... _Other2>
_Compressed_pair(_One_then_variadic_args_t, _Other1&& _Val1, _Other2&&... _Val2)
: _Ty1(_STD forward<_Other1>(_Val1)), _Myval2(_STD forward<_Other2>(_Val2)...) {}
_Ty1& _Get_first() noexcept {
return *this;
}
const _Ty1& _Get_first() const noexcept {
return *this;
}
};
这个类的构造函数在初始化列表中先构造父类,然后初始化自己的数据成员。父类是个LambdaExpr
类。这个类里面有数据成员吗?看看案例中自定义的析构器[](Demo* p) { delete p; }
,[]
中没有绑定任何东西,所以该类显然没有数据成员。只要是有良心的 C++ 教材,应该会告诉初学者LambdaExpr
其实是个can call function object
,相当于类中有个operator()
,若不以&
或=
赋予LambdaExpr
状态的话,这个类是个空类,在MSVC
下sizeof(LamdaExpr) == 1
。而子类含一个数据,即Demo*
,尺寸与机器有关。
使用_Get_first()
方法可返回父类对应的can call function object
以便调用。
验证:
#include <memory>
#include <iostream>
int main()
{
auto pDemo = new Demo;
auto deleter = [](Demo* p) {delete p; };
std::_Compressed_pair<decltype(deleter), Demo*> pair(std::_One_then_variadic_args_t(), deleter, pDemo);
std::cout << sizeof(deleter) << 'n';
std::cout << sizeof(pair) << 'n';
std::cout << sizeof(pair._Myval2) << 'n';
pair._Get_first()(pDemo);
return 0;
}
x64
下输出为
1
8
8
所以说是个压缩的pair
,当然这只是一个特例化,注意看std::_Compressed_pair
的第三个模板参数,是做标签指派的,如果编译时值为false
,那这个压缩的pair
就名不副实了,就会成为普通意义上的pair
。
插曲结束,内存分布图为:
[3] 使用 std::make_shared
案例
#include <memory>
class Demo {};
int main()
{
auto pDemoUseMakeShared = std::make_shared<Demo>();
return 0;
}
分析
inline shared_ptr<Demo> make_shared() {
const auto _Rx = new _Ref_count_obj<Demo>();
shared_ptr<Demo> _Ret;
_Ret._Set_ptr_rep_and_enable_shared(_Rx->_Getptr(), _Rx);
return _Ret;
}
template <>
class _Ref_count_obj<Demo> : public _Ref_count_base {
public:
explicit _Ref_count_obj() : _Ref_count_base() {
::new (static_cast<void*>(&_Storage)) Demo();
}
private:
aligned_union_t<1, Demo> _Storage;
};
这里有个std::_Ref_count_obj<Demo>::_Storage
,这个深究比较复杂,暂时忽略,总之可看成一块内存。构造时用了placement new
,把实例塞进自己内部。这样一来可以清楚看到与pDemo
的区别。也是所谓被托管的对象与引用计数绑定的实现。而使用std::shared_ptr
构造则是两者分离的实现。构造函数体内的后续流程与基本构造就一致了,不再赘述。
这种构造不支持捆绑自定义的析构器。
[4] 父类为 std::enable_shared_from_this
案例
#include <memory>
class Demo : public std::enable_shared_from_this<Demo> {};
int main()
{
auto pDemoWithEnableShared = std::shared_ptr<Demo>(new Demo);
return 0;
}
由于多了父类,基础构造的new
时要先构造Demo
的父类,也就是std::enable_shared_from_this<Demo>
template <>
class enable_shared_from_this<Demo> {
public:
// ...
protected:
constexpr enable_shared_from_this() noexcept : _Wptr() {}
mutable weak_ptr<Demo> _Wptr;
};
template <class _Ty>
class weak_ptr : public _Ptr_base<_Ty> {
public:
constexpr weak_ptr() noexcept {}
}
注意到std::weak_ptr
也派生自std::_Ptr_base
,故类中会保存父类的两个指针数据成员。所以在std::shared_ptr
构造函数体执行之前的内存布局如下:
使用std::enable_shared_from_this
有一点,就是不要在子类Demo
的构造函数中调用shared_from_this()
。
shared_ptr<Demo> shared_ptr<Demo>::shared_from_this() { // return shared_ptr
return shared_ptr<Demo>(_Wptr);
}
观察内存布局图就一目了然。这时_Wptr
里都是空指针。使用一个空的std::weak_ptr
构造std::shared_ptr
马上抛异常。
接下来:
template <>
class shared_ptr<Demo> : public _Ptr_base<Demo> {
public:
explicit shared_ptr(Demo* _Px) {
_Set_ptr_rep_and_enable_shared(_Px, new _Ref_count<_Ux>(_Px));
}
}
// 由于继承自 std::enable_shared_from_this<Demo>,源码中的 if constexpr 满足条件,故多了设置 _Wptr的代码
void shared_ptr<Demo>::_Set_ptr_rep_and_enable_shared(Demo* const _Px, _Ref_count_base* const _Rx) noexcept {
this->_Set_ptr_rep(_Px, _Rx);
if (_Px && _Px->_Wptr.expired()) {
_Px->_Wptr = shared_ptr<Demo>(*this, _Px));
}
}
this->_Set_ptr_rep(_Px, _Rx)
设置std::shared_ptr
中的两个父类成员指向正确的对象。
而_Px->_Wptr = shared_ptr<Demo>(*this, _Px))
这行代码发生了很多事情,出于异常安全考虑, 首先构造一个临时std::shared_ptr
的构造,然后是std::weak_ptr
的move assignment(operator=)
,最后是临时std::shared_ptr
的析构。
shared_ptr<Demo>(*this, _Px))
里没有new _Ref_count<Demo>(_Px)
这样的语句,所以不会创建新的Control Block
。而错误的用法std::shared_ptr<Demo>(this)
则会。可见,不同的构造函数行为完全不同。
无须跟踪源码可知,首先_Uses
一增一减没有发生变化,其次std::shared_ptr
将自己父类中的_Ptr
与_Rep
转交给std::weak_ptr
,而原_Wptr
中两者均为空指针,交换后可以直接丢弃(当然_Rep
是被安全析构的,只不过是delete nullptr
)即可。由于增加了一个std::weak_ptr
来管理Control Block
,故_Weaks
+ 1 为 2。
还是出于异常安全考虑,源码在std::weak_ptr
的move assignment(operator=)
中还构造了个临时的std::weak_ptr
实施std::swap
,不过这对最终结果没有影响。
从main()
函数开始一直到里面的object
中的_Wptr
被赋值结束,构造完成。整个过程只有一次new _Ref_count<T>
,只有一个Control Block
。
因此最终版本为:
简化后为:
为什么使用shared_from_this()
可以安全地获取?只要不是从裸指针构造std::shared_ptr
就不会构造Control Blcok
。多了std::shared_ptr
就会使_Uses
自增 1 为 2。两个引用计数的自增操作均为原子操作。
#include <memory>
class Demo : public std::enable_shared_from_this<Demo> {};
int main() {
auto pDemoWithEnableShared = std::shared_ptr<Demo>(new Demo);
auto pDemoSharedFromOwn = pDemoWithEnableShared->shared_from_this();
return 0;
}
布局:
没有增加Control Blcok
,内存管理正常。
可不可以不继承std::enable_shared_from_this
,又避免创建两个Control Block
?那只能在一个地方使用裸指针构造,比如在类内std::shared_ptr<Demo>(this)
。那类外势必得使用这个返回的std::shared_ptr
创建新的std::shared_ptr
,但是这样已经不是原来的设计意图了,在类内的这个工作没有任何价值,结果就是显式创建两个std::shared_ptr
共享一个实例而已。所以为了优雅地共享以达到设计目的,必须继承std::enable_shared_from_this
。
这其实是一种侵入式的设计,即用户必须在自定义类中安插成员(函数或数据)以完成设计者的意图,而继承某个定义好的基类是安插成员的经典方法。
至此,标准库提供的组件介绍完毕。接下来逛逛预备部队boost
。
[5] boost::intrusive_ptr
template<class T>
class intrusive_ptr {
public:
intrusive_ptr(T* p, bool add_ref = true) : px(p) {
if (px != 0 && add_ref) {
intrusive_ptr_add_ref(px);
}
}
~intrusive_ptr() {
if (px != 0) {
intrusive_ptr_release(px);
}
}
private:
T * px;
}
这个智能指针可谓非常懒惰了,构造与析构分别委托给两个函数。而这两个函数由用户提供。用户需定义一个父类作为中间类,简单含引用计数即可。两个函数声明为友元。
template<class T>
class intrusive_ptr_base {
public:
intrusive_ptr_base() : ref_count(0) {}
friend void intrusive_ptr_add_ref(intrusive_ptr_base<T> const* p) {
++p->ref_count;
}
friend void intrusive_ptr_release(intrusive_ptr_base<T> const* p) {
if (--p->ref_count == 0) {
boost::checked_delete(static_cast<T const*>(s));
}
}
boost::intrusive_ptr<T> self() {
return boost::intrusive_ptr<T>((T*)this);
}
private:
mutable boost::detail::atomic_count ref_count;
};
所以boost::intrusive_ptr
构造时做的唯一一件事就是增加引用计数,析构时减少引用计数,减为 0 时销毁对象。
最后实际工作的类派生于intrusive_ptr_base
即可。由于Object
自己管理自己的生命周期,所以可以随意使用boost::intrusive_ptr<T>(raw_pointer)
,包括boost::intrusive_ptr<T>(this)
。
内存布局:
智能指针是个有趣的话题,在 C++ 看来是活用 RAII 来托管对象的生命周期。这些智能指针都依托于引用计数,其实是内存管理与垃圾回收的一种工具。更多 GC 细节,在其他高级语言如 Java、C# 等是热门话题。