1. shared_ptr
虽然早就对 shared_ptr
的原理烂熟于心,手撕也没少做过,但有时候总感觉对其还是很陌生;
在看《Linux 多线程服务端编程》时,作者提到一个析构动作在创建时被捕获又彻底把我搞懵了,终于下定决心要研究下 GCC 9.4 中 tr1
下的源码;
1.1 基本架构
shared_ptr<Tp> sp(new Tp1)
,之后假设 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
最好是管理堆上的对象,而非栈上的;
1.2 enable_shared_from_this 的作用
shard_ptr<Tp> sp(new Tp1)
,这里假设 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
(6)使用 shared_from_this()
前提条件:当前对象已经存在一个关联的控制块
要想符合设计依据的情况,必须已经存在一个指向当前对象的std::shared_ptr(比如调用 shared_from_this 的成员函数外面已经存在一个std::shared_ptr
)。如果没有std::shared_ptr
指向当前对象(即当前对象没有关联控制块),行为是未定义的,shared_from_this 通常抛出一个异常。
因此,继承自 std::enable_shared_from_this
的类通常将它们的构造函数声明为 private,并且让客户端通过返回 std::shared_ptr
的工厂函数创建对象
1.3 对象释放、控制块不释放的情况
(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<MtestSize> sp1(mwp);
}
上述输出如下,可见确实执行了析构函数,但控制块还没有释放,通过 mwp
还能够访问控制块;
construct
refcount: 1
destory
current refcount: 0
sp is empty
1.4 析构动作在创建时被捕获(简单复现)
听起来很高大上,实际原理很简单;
(1)核心:设法保存两种类型的指针,一个是 Tp
类型的,另一个是 Tp1
类型的;析构时,调用 Tp1
的析构函数即可;
形如 template<typename Tp, typename Tp1> class shared_ptr
是最简单的实现形式,但这不符合 STL 的 shared_ptr
;
(2)与实际的对应关系
Mcatchbase
对应 _Sp_counted_base
McatchImpl
对应 _Sp_counted_base_impl
Mcatchtest
对应 __shared_count
(3)实现心得
STL 采用了 template<typename Tp> class shared_ptr
的形式,对用户比较友好;
这种情况下我们需要另一个类 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.5 make_shared
(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<int>
、string
类型;
如果改为 vector<int>
类型
// 使用 vector<int> v = {1,2,3,4,5} 进行初始化
cout << "address: " << (_pi) << " size: " << sizeof(*_pi) << endl;
// 输出为 40;
这里依旧可以确定下来,因为 vector<int>
本质上内部是三个指针,其本身大小是不会改变的,尽管指针的指向可能会改变;
1.6 异常安全问题
尽量不要使用匿名的 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() );
在上面第一种情况下的可能评估顺序为:
- allocate memory for int
- construct int
- call g()
- construct shared_ptr< int >
- call f()
如果在上述第 3 步抛出异常,那么之前 new
的对象可能不会释放(标准中无要求),造成了内存的泄漏;
只能保证在 new
构造对象异常时,可以释放之前申请的内存;
1.7 自定义删除器
auto loggingDel = [](Widget *pw) //自定义删除器
{ //(和条款18一样)
makeLogEntry(pw);
delete pw;
};
std::unique_ptr< //删除器类型是
Widget, decltype(loggingDel) //指针类型的一部分
> upw(new Widget, loggingDel);
std::shared_ptr<Widget> //删除器类型不是
spw(new Widget, loggingDel); //指针类型的一部分
指定自定义删除器不会改变 std::shared_ptr
对象的大小,不管删除器是什么。
(1)什么时候创建控制块?
std::make_shared
总是创建一个控制块。它创建一个要指向的新对象,所以可以肯定 std::make_shared
调用时对象不存在其他控制块。
当从独占指针(即 std::unique_ptr
或者原始指针)上构造出 std::shared_ptr
时会创建控制块。
用 std::shared_ptr
或者 std::weak_ptr
作为构造函数实参创建 std::shared_ptr
不会创建新控制块
1.8 unique_ptr
(1)std::unique_ptr
大小等同于原始指针,而且对于大多数操作(包括取消引用),他们执行的指令完全相同
-
当使用默认删除器时(如delete),你可以合理假设 std::unique_ptr 对象和原始指针大小相同。
-
当自定义删除器时,情况可能不再如此。函数指针形式的删除器,通常会使 std::unique_ptr 的大小从一个字(word)增加到两个。对于函数对象形式的删除器来说,变化的大小取决于函数对象中存储的状态多少;
-
无状态函数(stateless function)对象(比如不捕获变量的lambda表达式)对大小没有影响,这意味当自定义删除器可以实现为函数或者lambda时,尽量使用lambda:
(2)std::unique_ptr
可以管理数组,而 std::shared_ptr
不行;管理数组的特性很少被使用,尽可以用vector
等替代;
1.9 weak_ptr 使用场景
用std::weak_ptr
替代可能会悬空的std::shared_ptr
std::weak_ptr
的潜在使用场景包括:缓存、观察者列表、打破 std::shared_ptr
环状结构
1.10 make 函数
(1)std::make_unique
和 std::make_shared
接收任意的多参数集合,完美转发到构造函数去动态分配一个对象,然后返回这个指向这个对象的指针。
(2)第三个make函数是 std::allocate_shared
它行为和 std::make_shared
一样,只不过第一个参数是用来动态分配内存的 allocator 对象。
(3)make 函数优点
避免异常问题;
只需一次动态分配,效率提升;
processWidget(std::shared_ptr<Widget>(new Widget), //潜在的资源泄漏!
computePriority());
processWidget(std::make_shared<Widget>(), //没有潜在的资源泄漏
computePriority());
(4)make 函数存在的问题
不能自定义删除器
大括号初始化无法完美转发
(下列只对 std::make_shared
)
使用 make 函数去创建重载了operator new和operator delete类的对象是糟糕的想法
直到控制块的内存也被销毁,对象占用的内存才被释放
1.11 Pimpl惯用法
Pimpl 惯用法通过减少在类实现和类使用者之间的编译依赖来减少编译时间。
对于 std::unique_ptr 类型的 pImp l指针,需要在头文件的类里声明特殊的成员函数,但是在实现文件里面来实现他们。即使是编译器自动生成的代码可以工作,也要这么做。
以上的建议只适用于 std::unique_ptr,不适用于 std::shared_ptr。
参考
https://www.boost.org/doc/libs/1_84_0/libs/smart_ptr/doc/html/smart_ptr.html#shared_ptr_best_practices
《Effective Modern C++》