STL源码分析:shared_ptr 和 weak_ptr

1. shared_ptr

虽然早就对 shared_ptr 的原理烂熟于心,手撕也没少做过,但有时候总感觉对其还是很陌生;

在看《Linux 多线程服务端编程》时,作者提到一个析构动作在创建时被捕获又彻底把我搞懵了,终于下定决心要研究下 GCC 9.4 中 tr1 下的源码;

1.1 基本架构

shared_ptr sp(new Tp1),之后假设 TpTp1 是相容的(可简单认为 TpTp1的基类),并且假设 Tp1 位于堆区;

下图为使用原始指针构造的shared_ptr,各个类的关系如下图所示,因此若用原始指针构造多个 shared_ptr,那么就会有多个控制块,即下图中的_Sp_counted_base_impl,虽然它们都会指向相同的实际对象,但引用计数是不互通的(这会导致重复释放问题):
STL源码分析:shared_ptr 和 weak_ptr_第1张图片

(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 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 的重载函数如下所示,当 _Tp1enable_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 构造)

STL源码分析:shared_ptr 和 weak_ptr_第2张图片

(3)这里 _M_weak_count = 2 如何确保控制块会被释放?

shared_ptr 析构时,会调用 __shared_count 的析构函数

  1. _M_use_count 从 1 变为 0,此时会调用对象的析构函数,这时 _M_weak_this 是实际对象基类的成员,也会被析构掉,从而保证 _M_weak_count 能够变为 1
  2. 再将 _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.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 sp1(mwp);
}

上述输出如下,可见确实执行了析构函数,但控制块还没有释放,通过 mwp 还能够访问控制块;

construct  
refcount:  1
destory  
current refcount:  0
sp is empty 

1.4 析构动作在创建时被捕获(简单复现)

听起来很高大上,实际原理很简单;

(1)核心:设法保存两种类型的指针,一个是 Tp 类型的,另一个是 Tp1 类型的;析构时,调用 Tp1 的析构函数即可;

形如 template class shared_ptr 是最简单的实现形式,但这不符合 STL 的 shared_ptr

(2)与实际的对应关系

Mcatchbase 对应 _Sp_counted_base
McatchImpl 对应 _Sp_counted_base_impl
Mcatchtest 对应 __shared_count

(3)实现心得

STL 采用了 template 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限定符(即 constvolatile)。

注意,它的作用对象是对象,对指针不起作用;

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

STL源码分析:shared_ptr 和 weak_ptr_第3张图片

// 这里的_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 的实际大小,从而给它分配合适大小内存(其可能是变长类型),例如 vectorstring类型;

如果改为 vector 类型

// 使用 vector v = {1,2,3,4,5} 进行初始化
cout << "address: " << (_pi) << "  size: " << sizeof(*_pi) << endl;
// 输出为 40;

这里依旧可以确定下来,因为 vector 本质上内部是三个指针,其本身大小是不会改变的,尽管指针的指向可能会改变;

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() );

在上面第一种情况下的可能评估顺序为:

  1. allocate memory for int
  2. construct int
  3. call g()
  4. construct shared_ptr< int >
  5. call f()

如果在上述第 3 步抛出异常,那么之前 new 的对象可能不会释放(标准中无要求),造成了内存的泄漏;

只能保证在 new 构造对象异常时,可以释放之前申请的内存;

参考

https://www.boost.org/doc/libs/1_84_0/libs/smart_ptr/doc/html/smart_ptr.html#shared_ptr_best_practices

你可能感兴趣的:(C++,c++,linux,stl)