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。