虽然早就对 shared_ptr
的原理烂熟于心,手撕也没少做过,但有时候总感觉对其还是很陌生;
在看《Linux 多线程服务端编程》时,作者提到一个析构动作在创建时被捕获又彻底把我搞懵了,终于下定决心要研究下 GCC 9.4 中 tr1
下的源码;
shared_ptr
,之后假设 Tp
和 Tp1
是相容的(可简单认为 Tp
是 Tp1
的基类),并且假设 Tp1
位于堆区;
下图为使用原始指针构造的shared_ptr
,各个类的关系如下图所示,因此若用原始指针构造多个 shared_ptr
,那么就会有多个控制块,即下图中的_Sp_counted_base_impl
,虽然它们都会指向相同的实际对象,但引用计数是不互通的(这会导致重复释放问题):
(1)remove_pointer
的作用,利用模板自动推导,获得指针指向元素的类型或元素类型(T 本身不是指针),即获得 Tp1 的类型;
(2) _M_use_count
和 _M_weak_count
,加减为原子操作
_M_use_count
为 0 时,调用 _M_dispose()
,即上图中的仿函数,调用指向对象的析构函数;
_M_weak_count
为 0 时,调用 _M_destroy()
,删除掉控制块;
(3)拷贝构造函数:shared_ptr
拷贝构造时,_M_pi
指向同一个控制块,_M_use_count
加 1;
(4)注意这里的默认析构函数,使用了delete
,因此 shared_ptr
最好是管理堆上的对象,而非栈上的;
shard_ptr
,这里假设 Tp1
继承自 enable_shared_from_this
(1)在构造 __shared_ptr
类时,初始化 _M_ptr
和 _M_refcount
后会调用如下函数
// 其中 _M_refcount 为 __shared_count<_Lp> 类型,__p 为 _Tp1 类型
__enable_shared_from_this_helper(_M_refcount, __p, __p);
而 __enable_shared_from_this_helper
的重载函数如下所示,当 _Tp1
跟 enable_shared_from_this
没有继承关系时,会匹配下面第 2 个函数,即什么也不做
当 _Tp1
继承自 enable_shared_from_this
时,会匹配下面第 1 个函数,_Tp1* 容易转换为 基类指针;
// Friend of enable_shared_from_this.
template<typename _Tp1, typename _Tp2>
void
__enable_shared_from_this_helper(const __shared_count<>&,
const enable_shared_from_this<_Tp1>*,
const _Tp2*);
template<_Lock_policy _Lp>
inline void
__enable_shared_from_this_helper(const __shared_count<_Lp>&, ...)
{ }
(2)在 __enable_shared_from_this_helper
函数中,主要是将设置下图中 1 和 2 指针,并且 _M_weak_count
加 1,结果为 _M_weak_count = 2
、_M_use_count = 1
;
使用 shared_ptr
构造 weak_ptr
类似上述过程;( weak_ptr
自身是无法创建出来控制块的,因此只能拷贝构造或者从 shared_ptr
构造)
(3)这里 _M_weak_count = 2
如何确保控制块会被释放?
等 shared_ptr
析构时,会调用 __shared_count
的析构函数
_M_use_count
从 1 变为 0,此时会调用对象的析构函数,这时 _M_weak_this
是实际对象基类的成员,也会被析构掉,从而保证 _M_weak_count
能够变为 1_M_weak_count
减 1,变为 0 后释放控制块(4)调用 shared_from_this()
时,会使用 _M_weak_this
构造一个新的 shared_ptr
,这个比较容易实现因为二者各种类成员变量几乎一样;
唯一不同的是,会使得 _M_use_count
加 1
这也解释了为什么继承自 enable_shared_from_this
后,就能生成与初始 shared_ptr
共享所有权的 shared_ptr
,因为对象内部就有指向对应控制块的指针,并且由于是 weak_ptr
类型不会造成循环引用问题;
(5)Tp1
继承自 enable_shared_from_this
的代价是,会引入两个指针,一个指向实际对象,另一个指向 控制块
测试其大小程序如下,符合推测:
class MtestSize : public std::enable_shared_from_this<MtestSize>
{
public:
std::shared_ptr<Good> getptr()
{
return shared_from_this();
}
int* x;
};
class MtestSize1
{
public:
int* x;
};
MtestSize mg;
MtestSize1 mg1;
cout << " MtestSize: " << sizeof(mg) << " MtestSize1: " << sizeof(mg1) << endl;
输出内容: MtestSize: 24 ; MtestSize1: 8
(1)使用 shared_ptr
构造了其他的weak_ptr
,就会导致_M_use_count = 0
时控制块不释放的情况:正因为有这个机制的存在,我们可以通过 weak_ptr
探知对象是否还存活(最好使用lock()
函数不会抛出异常);
class MtestSize
{
public:
MtestSize() {
cout << " construct " << endl;
}
~MtestSize() {
cout << " destory " << endl;
}
int* x;
};
void write()
{
weak_ptr<MtestSize> mwp;
{
std::shared_ptr<MtestSize> mp(new MtestSize());
mwp = mp;
cout << " refcount: " << mp.use_count() << endl;
}
cout << "current refcount: " << mwp.use_count() << endl;
// 不会抛出异常,因为其先判断当前引用计数是否为 0,若为 0,则返回默认构造函数,即空类型;
std::shared_ptr<MtestSize> sp(mwp.lock());
if( !sp ) {
cout << " sp is empty " << endl;
}
// 指向对象析构后,会抛出异常 std::bad_weak_ptr
// std::shared_ptr sp1(mwp);
}
上述输出如下,可见确实执行了析构函数,但控制块还没有释放,通过 mwp
还能够访问控制块;
construct
refcount: 1
destory
current refcount: 0
sp is empty
听起来很高大上,实际原理很简单;
(1)核心:设法保存两种类型的指针,一个是 Tp
类型的,另一个是 Tp1
类型的;析构时,调用 Tp1
的析构函数即可;
形如 template
是最简单的实现形式,但这不符合 STL 的 shared_ptr
;
(2)与实际的对应关系
Mcatchbase
对应 _Sp_counted_base
McatchImpl
对应 _Sp_counted_base_impl
Mcatchtest
对应 __shared_count
(3)实现心得
STL 采用了 template
的形式,对用户比较友好;
这种情况下我们需要另一个类 McatchImpl
来保存 Tp1
类型,但 McatchImpl
类无法成为 Mcatchtest
的成员变量,因为其有更多模板参数(至少会有一个 Tp1
),而 McatchImpl
只有一个 Tp
,解决办法就是使用基类指针指向子类(即 Mcatchbase
的作用 );
template<typename Tp1>
struct Mydelete
{
void operator()(Tp1* p) {
delete p;
}
};
template<typename Tp>
class Mcatchbase
{
public:
Tp* _opt;
virtual void dispose() {};
};
// 这里析构函数 Del 为模板参数,这样设计增强了其扩展性,因为可以为不同类型,如函数指针
template<typename Tp, typename Tp1, typename Del>
class McatchImpl: public Mcatchbase<Tp>
{
public:
McatchImpl(Tp* opt, Tp1* npt, Del del): _npt(npt),_del(del){}
void dispose() {
_del(_npt);
}
Tp1* _npt;
Del _del;
};
template<typename Tp>
class Mcatchtest
{
public:
template<typename Tptr>
Mcatchtest(Tptr p) : _opr(p){
typedef typename std::remove_pointer<Tptr>::type _Tp1;
_bs = new McatchImpl<Tp, _Tp1, Mydelete<_Tp1> >( nullptr, p, Mydelete<_Tp1>());
cout << "catch construct " << endl;
}
~Mcatchtest() {
_bs->dispose();
delete _bs;
cout << "catch destory " << endl;
}
Tp* _opr;
Mcatchbase<Tp>* _bs;
};
上述实现进一步简化:
template<typename Tp>
class Mcatchbase
{
public:
Tp* _opt;
virtual void dispose() {};
};
template<typename Tp, typename Tp1>
class McatchImpl: public Mcatchbase<Tp>
{
public:
McatchImpl(Tp* opt, Tp1* npt): _npt(npt){}
void dispose() {
delete _npt;
}
Tp1* _npt;
};
template<typename Tp>
class Mcatchtest
{
public:
template<typename Tptr>
Mcatchtest(Tptr p) : _opr(p){
typedef typename std::remove_pointer<Tptr>::type _Tp1;
_bs = new McatchImpl<Tp, _Tp1 >( nullptr, p);
cout << "catch construct " << endl;
}
~Mcatchtest() {
_bs->dispose();
delete _bs;
cout << "catch destory " << endl;
}
Tp* _opr;
Mcatchbase<Tp>* _bs;
};
测试代码如下所示:
class Mbase
{
public:
Mbase() {
cout << "base construct " << endl;
}
~Mbase() { // 注意此处不是虚函数
cout << "base destory " << endl;
}
};
class MSubbase: public Mbase
{
public:
MSubbase() {
cout << "Sub construct " << endl;
}
~MSubbase() {
cout << "Sub destory " << endl;
}
};
void write()
{
Mbase* ptr = new MSubbase();
delete ptr;
cout << "------------------------" << endl;
// 即使没有基类析构不是虚函数,也能得到正确的析构顺序
Mcatchtest<Mbase> mt( new MSubbase() );
}
输出为:
base construct
Sub construct
base destory
------------------------
base construct
Sub construct
catch construct
Sub destory
base destory
catch destory
(1)remove_cv
删除了其最顶端的cv限定符(即 const
和 volatile
)。
注意,它的作用对象是对象,对指针不起作用;
cout << std::is_same_v<std::remove_cv_t<const int>, int> << endl; // 1
cout << std::is_same_v<std::remove_cv_t<const int*>, int*> << endl; // 0 这个 const 是修饰指针的
cout << std::is_same_v<std::remove_cv_t<int* const>, int*> << endl; // 1
(2)其实就是一块去申请内存
如下图所示:_Sp_counted_ptr_inplace
其就三个成员变量,两个引用计数 + 一个实际对象;
验证代码如下所示,可以修改类型自行验证(不要忘记内存对齐的影响):
std::allocator<int[4]> ialloc;
_Sp_counted_ptr_inplace<int[4], std::allocator<int[4]>, __default_lock_policy> ss(ialloc);
cout << sizeof(ss) << endl; // 32 --> 4*4 + 8 + 8
// 这里的_Alloc 为下述类型
std::allocator<_Tp_nc>
class _Sp_counted_ptr_inplace ...{
using __allocator_type = __alloc_rebind<_Alloc, _Sp_counted_ptr_inplace>;
}
// 这里 typename 的作用是告诉编译器 _Sp_cp_type::__allocator_type 是一个类型而非变量
typename _Sp_cp_type::__allocator_type __a2(__a._M_a);
// 因此 __a2 的 类型为 __alloc_rebind<_Alloc, _Sp_counted_ptr_inplace>
// 在该函数内部会调用 allocate 分配一个大小为 sizeof(Alloc::value_type) 的内存空间
auto __guard = std::__allocate_guarded(__a2);
// 在这块内存上进行构造
去分析 __alloc_rebind
这些东西太麻烦,各种类的包装看的眼花缭乱。。。,这里只能从其表现出的行为去分析一下了
(3)测试代码如下
class Mbase
{
public:
Mbase(std::allocator<int> ialloc, int y) {
allocator_traits<std::allocator<int>>::construct(ialloc, _M_storage._M_ptr(), y);
}
int* _x;
int* _y;
// 这里一定要用该类型,来保证是内存对齐的;
__gnu_cxx::__aligned_buffer<int> _M_storage;
};
std::allocator<int> ialloc;
__alloc_rebind<std::allocator<int>, Mbase> ss(ialloc);
auto __guard = std::__allocate_guarded(ss);
Mbase* _mem = __guard.get();
auto _pi = new (_mem) Mbase(ialloc, 5);
cout << "address : " << (_pi) << endl;
cout << "address : " << (_pi->_M_storage._M_ptr()) << " val: " << *(_pi->_M_storage._M_ptr()) << endl;
address : 0x603000000040
address : 0x603000000050 val: 5
由输出可见,这两个对象被放在了一块;据此推测 __alloc_rebind
的作用就是重新绑定对象类型,在本例中是将int
换为 Mbase
,因此申请空间时会分配 sizeof(Mbase)
大小的空间;
(4)这里还存在一个问题,
调用 allocate
时如何知道对象 Tp
的实际大小,从而给它分配合适大小内存(其可能是变长类型),例如 vector
、string
类型;
如果改为 vector
类型
// 使用 vector v = {1,2,3,4,5} 进行初始化
cout << "address: " << (_pi) << " size: " << sizeof(*_pi) << endl;
// 输出为 40;
这里依旧可以确定下来,因为 vector
本质上内部是三个指针,其本身大小是不会改变的,尽管指针的指向可能会改变;
尽量不要使用匿名的 shared_ptr
,可能会造成内存泄漏;
函数参数的评估顺序是不确定的,expr1 可以先于、晚于、交错 expr2 的评估
只能保证 expr1 和 expr2 的评估会早于 f() 的调用
f( expr1, expr2 );
// 一.差的方式
f( shared_ptr<int>( new int(2) ), g() );
// 二.好的方式
shared_ptr<int> p( new int(2) );
f( p, g() );
在上面第一种情况下的可能评估顺序为:
如果在上述第 3 步抛出异常,那么之前 new
的对象可能不会释放(标准中无要求),造成了内存的泄漏;
只能保证在 new
构造对象异常时,可以释放之前申请的内存;
https://www.boost.org/doc/libs/1_84_0/libs/smart_ptr/doc/html/smart_ptr.html#shared_ptr_best_practices