从源码看std::weak_ptr

1. 序

本篇文章来讲解一下weak_ptr, weak_ptr一般也都是和shared_ptr同时存在的,相当于对对象的弱化版引用。我们首先从源码的角度来讲解下weak_ptr,然后再讲一下weak_ptr的常见用法。源码采用的是msvc编译器实现说,应该各个编译器实现原理差不多。

2. 代码分析

2.1 代码结构

template <class _Ty>
class weak_ptr : public _Ptr_base<_Ty> { // class for pointer to reference counted resource
public:
    constexpr weak_ptr() noexcept {}

    // ...(略)
};

首先weak_ptr是个模板,_Ty就是指向对象的类型。然后看它是继承 _Ptr_base这个基类,shared_ptr同样也继承了这个类,从这里也可以看出来weak_ptr和shared_ptr其实是密不可分的。然后看他的无参的构造函数,要注意的是他的构造函数是在编译期就可以构造出来的。

template <class _Ty>
class _Ptr_base { // base class for shared_ptr and weak_ptr
public:
    using element_type = remove_extent_t<_Ty>;
    
    // ...(略)
    
private:
    element_type* _Ptr{nullptr};
    _Ref_count_base* _Rep{nullptr};
};

我们看到element_type* _Ptr其实就是指向要保存对象的指针(eg. weak_ptr element_type就是int), _Ref_count_base这个对象就是保存引用计数的对象,再来详细看一下 _Ref_count_base:

class __declspec(novtable) _Ref_count_base { // common code for reference counting
private:
    virtual void _Destroy() noexcept     = 0; // destroy managed resource
    virtual void _Delete_this() noexcept = 0; // destroy self

    _Atomic_counter_t _Uses  = 1;
    _Atomic_counter_t _Weaks = 1;
    
    // ...(略)
    
    void _Incref() noexcept { // increment use count
        // ...(略)
    }
    
    void _Incwref() noexcept { // increment weak reference count
        // ...(略)
    }

    void _Decref() noexcept { // decrement use count
        // ...(略)
    }

    void _Decwref() noexcept { // decrement weak reference count
        // ...(略)
    }

    long _Use_count() const noexcept {
        return static_cast<long>(_Uses);
    }

    virtual void* _Get_deleter(const type_info&) const noexcept {
        return nullptr;
    }
};

_Ref_count_base成员变量有两个 _Uses和 _Weaks,一个是shared_ptr的引用计数,一个是weak_ptr的引用计数,初始化均为1。然后定义了一些实际的操作,包括增加减少引用计数( _Uses和 _Weaks相对应的)。这里的 _Use_count就是我们使用shared_ptr获取use_count的具体实现。

然后我们再回到 _Ptr_base这个类, _Rep仅仅是指向 _Ref_count_base的指针,实际在示例化的对象肯定是继承 _Ref_count_base这个类,我们能够从share_ptr中知道示例化的对象实际上是 _Ref_count这个类

template <class _Ty>
class _Ref_count : public _Ref_count_base { // handle reference counting for pointer without deleter
public:
    explicit _Ref_count(_Ty* _Px) : _Ref_count_base(), _Ptr(_Px) {}

private:
    virtual void _Destroy() noexcept override { // destroy managed resource
        delete _Ptr;
    }

    virtual void _Delete_this() noexcept override { // destroy self
        delete this;
    }

    _Ty* _Ptr;
};

这里这个也保存了一份指向实际对象的指针(_Ptr)。

通过一个图来帮助大家理解:

从对象(Object)构造了一个shared_ptr时(eg. auto ptr = std::make_shared(0)),_Ptr指向 _Red_count, _Rep指向Object(也即 _Ty)。weak_ptr无法从对象那里构造,只能从shared_ptr或者自身拷贝。

2.2 代码实现

整体的结构我们知道了,接下来我们就来分析weak_ptr的代码了。

template <class _Ty>
class weak_ptr : public _Ptr_base<_Ty> { // class for pointer to reference counted resource
public:
    constexpr weak_ptr() noexcept {}

    weak_ptr(const weak_ptr& _Other) noexcept {
        this->_Weakly_construct_from(_Other); // same type, no conversion
    }

    template <class _Ty2, enable_if_t<_SP_pointer_compatible<_Ty2, _Ty>::value, int> = 0>
    weak_ptr(const shared_ptr<_Ty2>& _Other) noexcept {
        this->_Weakly_construct_from(_Other); // shared_ptr keeps resource alive during conversion
    }
  
    // ...(略)
};

template <class _Ty>
class _Ptr_base {
	// ...(略)
	template <class _Ty2>
    void _Weakly_construct_from(const _Ptr_base<_Ty2>& _Other) noexcept { // implement weak_ptr's ctors
        if (_Other._Rep) {
            _Ptr = _Other._Ptr;
            _Rep = _Other._Rep;
            _Rep->_Incwref();
        } else {
            _STL_INTERNAL_CHECK(!_Ptr && !_Rep);
        }
    }
    // ...(略)
};

然后我们看weak_ptr中的使用拷贝构造函数及参数是shared_ptr来构造weak_ptr, 都调用 _Weakly_construct_from函数,然后 _Ty2其实表示可以转化为 _Ty的类型( _SP_pointer_compatible),如果 _Ty2不能转化为 _Ty,enable_if_t那里就会编译报错。举例来说这里是子类( _Ty2)转化为父类( _Ty)

然后看 _Weakly_construct_from的实现,也仅仅是把相应的值赋给weak_ptr的 _Ptr和 _Rep(从父类继承下来的),然后增加weak的引用计数。我们看到这里构造一个weak_ptr不会增加use的计数,只会增加weak的计数。 _STL_INTERNAL_CHECK这个是做check使用的,一些情况下可能会报错等吧,可以不用关注。

然后再继续weak_ptr中的代码:

template <class _Ty>
class weak_ptr : public _Ptr_base<_Ty> { // class for pointer to reference counted resource
public:
    // ...(略)
  
	template <class _Ty2, enable_if_t<_SP_pointer_compatible<_Ty2, _Ty>::value, int> = 0>
    weak_ptr(const weak_ptr<_Ty2>& _Other) noexcept {
        this->_Weakly_convert_lvalue_avoiding_expired_conversions(_Other);
    }
  
    // ...(略)
};

template <class _Ty>
class _Ptr_base {
    // ...(略)
    
	template <class _Ty2>
    void _Weakly_convert_lvalue_avoiding_expired_conversions(const _Ptr_base<_Ty2>& _Other) noexcept {
        // implement weak_ptr's copy converting ctor
        if (_Other._Rep) {
            _Rep = _Other._Rep; // always share ownership
            _Rep->_Incwref();

            if (_Rep->_Incref_nz()) {
                _Ptr = _Other._Ptr; // keep resource alive during conversion, handling virtual inheritance
                _Rep->_Decref();
            } else {
                _STL_INTERNAL_CHECK(!_Ptr);
            }
        } else {
            _STL_INTERNAL_CHECK(!_Ptr && !_Rep);
        }
    }
    // ...(略)
};

这块代码比较难理解,仅仅只是从weak_ptr< _Ty2>来构造weak_ptr< _Ty>,当然这里 _Ty2可以转化为 _Ty,这里可以转化是指可以直接赋值的,比如说 _Ty *ptr = ptr2;(ptr2是 _Ty2)格式的。所以一般来说就是子类赋值给父类,这个拷贝构造函数调用 _Weakly_convert_lvalue_avoiding_expired_conversions函数来实现,这个函数也是在 _Ptr_base中实现,最开始赋值引用计数和增加weak计数和 _Weakly_construct_from函数类似,只是下边这一段不好理解:

if (_Rep->_Incref_nz()) {
	_Ptr = _Other._Ptr; // keep resource alive during conversion, handling virtual inheritance
	_Rep->_Decref();
} 

我们能够看出来为了保证 _Ptr的赋值成功,首先要确认资源是存活的。

如何确认资源是存活的呢,我们看到赋值之前先执行_Incref_nz(use引用计数不为0时加1),赋值完成之后再减1,恢复原样。那这里加1就能保证在 _Ptr的赋值期间这块资源时存活的。因为 _Ptr这块资源是在use计数为0时释放,这里保证不为0就可以保证不释放。

然后进行 _Ptr 的赋值,我们这里还是不明白为什么要保证资源时存活的呢,这个指针的赋值和之前函数的的指针赋值有什么不一样吗?

还真是不一样,我们看下它注释,是为了处理虚继承时的情况,也就是说子类如果时虚继承父类,在子类指针赋值给父类指针的时候,这里的操作很复杂,因为需要调整虚表从子类的虚表中获取到父类的信息并截取出来。这个操作不能被打断,进一步保证线程安全。

再继续看weak_ptr的内容:

template <class _Ty>
class weak_ptr : public _Ptr_base<_Ty> { // class for pointer to reference counted resource
public:
    // ...(略)
  
	weak_ptr(weak_ptr&& _Other) noexcept {
        this->_Move_construct_from(_STD move(_Other));
    }

    template <class _Ty2, enable_if_t<_SP_pointer_compatible<_Ty2, _Ty>::value, int> = 0>
    weak_ptr(weak_ptr<_Ty2>&& _Other) noexcept {
        this->_Weakly_convert_rvalue_avoiding_expired_conversions(_STD move(_Other));
    }
  
    // ...(略)
};

template <class _Ty>
class _Ptr_base {
    // ...(略)
    
	template <class _Ty2>
    void _Move_construct_from(_Ptr_base<_Ty2>&& _Right) noexcept {
        // implement shared_ptr's (converting) move ctor and weak_ptr's move ctor
        _Ptr = _Right._Ptr;
        _Rep = _Right._Rep;

        _Right._Ptr = nullptr;
        _Right._Rep = nullptr;
    }
    // ...(略)
};

weak_ptr的移动构造函数比较简单,去调用_Move_construct_from函数,完成赋值后,Right的指针全部指控,达到转移的效果。

然后同理 _Ty2类型移动到Ty也需要保证资源是存活的,同样使用 _Weakly_convert_rvalue_avoiding_expired_conversions函数实现。

再进一步往下看weak_ptr的源码:

template <class _Ty>
class weak_ptr : public _Ptr_base<_Ty> { // class for pointer to reference counted resource
public:
    // ...(略)
	~weak_ptr() noexcept {
        this->_Decwref();
    }

    weak_ptr& operator=(const weak_ptr& _Right) noexcept {
        weak_ptr(_Right).swap(*this);
        return *this;
    }

    template <class _Ty2>
    weak_ptr& operator=(const weak_ptr<_Ty2>& _Right) noexcept {
        weak_ptr(_Right).swap(*this);
        return *this;
    }

    weak_ptr& operator=(weak_ptr&& _Right) noexcept {
        weak_ptr(_STD move(_Right)).swap(*this);
        return *this;
    }

    template <class _Ty2>
    weak_ptr& operator=(weak_ptr<_Ty2>&& _Right) noexcept {
        weak_ptr(_STD move(_Right)).swap(*this);
        return *this;
    }

    template <class _Ty2>
    weak_ptr& operator=(const shared_ptr<_Ty2>& _Right) noexcept {
        weak_ptr(_Right).swap(*this);
        return *this;
    }

    void reset() noexcept { // release resource, convert to null weak_ptr object
        weak_ptr{}.swap(*this);
    }

    void swap(weak_ptr& _Other) noexcept {
        this->_Swap(_Other);
    }
  
    // ...(略)
};

template <class _Ty>
class _Ptr_base {
    // ...(略)
    
	void _Incref() const noexcept {
        if (_Rep) {
            _Rep->_Incref();
        }
    }

    void _Decref() noexcept { // decrement reference count
        if (_Rep) {
            _Rep->_Decref();
        }
    }

    void _Swap(_Ptr_base& _Right) noexcept { // swap pointers
        _STD swap(_Ptr, _Right._Ptr);
        _STD swap(_Rep, _Right._Rep);
    }
    
    void _Incwref() const noexcept {
        if (_Rep) {
            _Rep->_Incwref();
        }
    }

    void _Decwref() noexcept { // decrement weak reference count
        if (_Rep) {
            _Rep->_Decwref();
        }
    }
    
    // ...(略)
};

把这一坨放在一起来说,代码也比较简单,当执行析构函数时,主要是对weak的计数减一,使用swap函数实现等号操作符的重写及重载。在参数是左值引用的时候,构造一个临时对象与当前对象swap,当前对象获取的就是临时对象的内容,临时对像在执行完函数会析构,当参数是右值引用的时候,直接使用swap函数,当前对象就会获得右值引用对象的内容,函数执行完后右值引用的这个对象也会析构。就达到了赋值(等号操作符)的效果。

_Ptr_base中罗列了增减user计数和weak计数的函数,我们去 _Rep的类里去看实现:

class __declspec(novtable) _Ref_count_base {
	virtual void _Destroy() noexcept     = 0; // destroy managed resource
    virtual void _Delete_this() noexcept = 0; // destroy self
    
    _Atomic_counter_t _Uses  = 1;
    _Atomic_counter_t _Weaks = 1;
    
public:
    bool _Incref_nz() noexcept { // increment use count if not zero, return true if successful
        auto& _Volatile_uses = reinterpret_cast<volatile long&>(_Uses);
        long _Count = __iso_volatile_load32(reinterpret_cast<volatile int*>(&_Volatile_uses));
        while (_Count != 0) {
            const long _Old_value = _INTRIN_RELAXED(_InterlockedCompareExchange)(&_Volatile_uses, _Count + 1, _Count);
            if (_Old_value == _Count) {
                return true;
            }

            _Count = _Old_value;
        }

        return false;
    }

    void _Incref() noexcept { // increment use count
        _MT_INCR(_Uses);
    }

    void _Incwref() noexcept { // increment weak reference count
        _MT_INCR(_Weaks);
    }

    void _Decref() noexcept { // decrement use count
        if (_MT_DECR(_Uses) == 0) {
            _Destroy();
            _Decwref();
        }
    }

    void _Decwref() noexcept { // decrement weak reference count
        if (_MT_DECR(_Weaks) == 0) {
            _Delete_this();
        }
    }
};

template <class _Ty>
class _Ref_count : public _Ref_count_base { // handle reference counting for pointer without deleter
public:
    explicit _Ref_count(_Ty* _Px) : _Ref_count_base(), _Ptr(_Px) {}

private:
    virtual void _Destroy() noexcept override { // destroy managed resource
        delete _Ptr;
    }

    virtual void _Delete_this() noexcept override { // destroy self
        delete this;
    }

    _Ty* _Ptr;
};

定义了 _Destroy和 _Delete_this接口,在 _Ref_count实现为 _Destroy是删除掉指向的指针, _Delete_this是删除掉引用计数的这个对象。

然后是 _MT_DECR 和 _MT_INC这个就是在windows中原子性加1和减1的操作。

然后大概看下 _Incref_nz这个函数,首先获取到 use这个计数的地址( _Volatile_uses),获取到它的值Count, _InterlockedCompareExchange函数意思是 _Volatile_uses中取值和Count比较,如果相等就Count+1赋值到 _Volatile_uses中,这里主要是在执行use计数加1这个过程中,如果有其他线程对use计数进行操作,因为这个函数的意义是不为0才加1,如果别的线程导致use变为0则不做操作。这里涉及到多线程,可能比较复杂,多理解几次就可以了。

最后我们要看的是_Decref和 _Decwref这两个函数,我们看到当use减到0时,会析构掉指向的对象,然后去讲weak计数减1,当weak计数减为0时,就会析构掉引用计数的对象。为什么weak和use这两个在use=0时会关联起来呢,只是因为引用计数对象( _Rep )初始时weak和use都是1,如果没有weak参与进来,只有 shared_ptr时,weak的计数就将一直是1,当use为0时,自然需要回收这个weak计数。

跳的比较远了,我们再回到weak_ptr中,还剩下最后两个比较关键的函数:

#define _NODISCARD [[nodiscard]]

template <class _Ty>
class weak_ptr : public _Ptr_base<_Ty> { // class for pointer to reference counted resource
public:
    // ...(略)
  
	_NODISCARD bool expired() const noexcept {
        return this->use_count() == 0;
    }

    _NODISCARD shared_ptr<_Ty> lock() const noexcept { // convert to shared_ptr
        shared_ptr<_Ty> _Ret;
        (void) _Ret._Construct_from_weak(*this);
        return _Ret;
    }
};

template <class _Ty>
class _Ptr_base { // base class for shared_ptr and weak_ptr
public:
    // ...(略)
    
    template <class _Ty2>
    bool _Construct_from_weak(const weak_ptr<_Ty2>& _Other) noexcept {
        // implement shared_ptr's ctor from weak_ptr, and weak_ptr::lock()
        if (_Other._Rep && _Other._Rep->_Incref_nz()) {
            _Ptr = _Other._Ptr;
            _Rep = _Other._Rep;
            return true;
        }

        return false;
    }
    
    // ...(略)
};

[[nodiscard]]是在C++17中加入的,表示如果以void来接收有返回值的函数,编译器会报一个警告,比如说这里:

#include <memeory>

int main() {
    std::weak_ptr<int> pt;
    bool rc = pt->expired(); // 不会报警告
    pt->expired();           // 会报警告
    
    return;
}

expired函数是获取use计数的值是否为0,也就是判断指向的对象资源是否已经被释放了。

lock函数表示会从weak_ptr指向的资源构造一个shared_ptr返回。我们看到它的实现如果资源还存活的话就会获得资源,否则就只能返回false了。

2.3 几个关键的点

  • weak_ptr也会有一个引用计数weak,和平常使用shared_ptr的引用计数use要区分开,use减为0时释放指向的对象,weak减为0时释放的引用计数(_Rep)类的对象

  • 无论时weak的引用计数还是use的引用计数,只有获取了指向的对象(_Ptr不为空)之后才是有效的,单独声明shared_ptr或者weak_ptr是没有计数这个概念的

  • weak_ptr并不会增加use的计数

3. 使用场景

从cppreference中我们知道,weak_ptr可以获得对象的临时所有权,用以跟踪对象。还有一个用法是打断shared_ptr管理的对象组成的环状引用

3.1 循环引用的计数问题

我们先来看循环引用的问题及如何解决:

#include <memory>
#include <iostream>

class B;
class A {
public:
    ~A() {
        std::cout << "~A" << std::endl;
    }

    void setPtr(std::shared_ptr<B>& b) {
        bPtr_ = b;
    }

private:
    std::shared_ptr<B> bPtr_;
};

class B {
public:
    ~B() {
        std::cout << "~B" << std::endl;
    }

    void setPtr(std::shared_ptr<A>& a) {
        aPtr_ = a;
    }

private:
    std::shared_ptr<A> aPtr_;
};

int main() {
    std::shared_ptr<A> aShared = std::make_shared<A>();
    std::shared_ptr<B> bShared = std::make_shared<B>();

    aShared->setPtr(bShared);
    bShared->setPtr(aShared);
    
    return 0;
}

看以上代码A对象保存了shared_ptr<B>, B对象像保存了shared_ptr<B>,在make_shared后,aShared和bShared的引用计数是1,然后分别设置成员变量后,aShared和bShared引用计数是2。但是main函数执行完后,aShared和bShared析构引用计数各减1,但也才是1,无法释放A对象和B对象。

因为对象相互持有,必须要有一个对象释放之后,才能进一步释放另一个。所以解决方案也比较简单,将其中一个成员变量由shared_ptr转为weak_ptr,因为weak_ptr持有对象时并不会增加use的引用计数。这里比如把A类中shared_ptr<B> 改为weak_ptr<B>即可,执行完setPtr后,aShared计数为2,bShared计数为1,释放时bShared首先减为0,进一步减少aPtr_的计数,aShared计数变为1,然后aShared析构,计数减为0,释放完成。

3.2 跟踪shared_ptr

这里使用cppreference的例子来说明:

#include <iostream>
#include <memory>
 
std::weak_ptr<int> gw;
void observe()
{
    std::cout << "use_count == " << gw.use_count() << ": ";
    if (auto spt = gw.lock()) { // 使用之前必须复制到 shared_ptr
		    std::cout << *spt << "\n";
    }
    else {
        std::cout << "gw is expired\n";
    }
}
 
int main()
{
    {
        auto sp = std::make_shared<int>(42);
        gw = sp;
        observe();
    }
 
    observe();
}

输出:
use_count == 1: 42
use_count == 0: gw is expired

sp获取到对象后,使用gw(weak_ptr)来得到shared_ptr的”跟踪权限“,也就是gw和sp指向相对的对象,执行一遍observe,我们知道gw.lock会构造一个shared_ptr出来,就能获取到sp指向的对象了。当sp析构了之后,再执行一遍observe,发现对象已经释放了。这里很好的起到了观察的作用。

4. 总结

本篇文章从源码角度讲解了weak_ptr,首先从代码整体结构带着大家一块梳理,然后从它和shared_ptr的关系,及如何构造,构造时的细节,析构等等。最后讲解了一下它的使用场景,相信大家看完之后能够更好的理解weak_ptr和shared_ptr,及更灵活的使用weak_ptr。

5.ref

https://zh.cppreference.com/w/cpp/memory/weak_ptr

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值