个人笔记——关于智能指针shared_ptr的一些事

导言

因为无聊,所以仔细看了看智能指针中shared_ptr的源码,以下是一些笔记。
需要说明的是,该源码源于编译器msvc的14.31.31103版本。由于是微软的编译器,自然带有许多其适配windows的宏,就不作详细的记录了。

正文

涉案类与SFINAE机制相关结构体

涉及这个问题的类(也就是用于ADT功能的数据结构,无特殊说明的话下同)很多,想要搞清楚shared_ptr的工作就不能避免先谈及这些。相关的类主要有这些:

  • _Prt_base
  • shared_ptr
  • weak_ptr
  • _Ref_count_base
  • _Ref_count
  • _Ref_count_resource
    虽然还有其他相关的类,但不提及也对shared_ptr工作原理的解释无太大影响。
    SFINAE机制相关结构体主要有这些:
  • enable_if_t
  • conjunction_v
    接下来,我将再说明一下类的继承与包含关系。

类继承关系

先画个图说明一下类的关系。有些类成员先不罗列,后面会提及。

public
public
public
public
_Prt_base
-element_type* _Ptr
-_Ref_count_base* _Rep
shared_ptr
weak_ptr
_Ref_count_base
-_Atomic_counter_t _Uses
-_Atomic_counter_t _Weaks
_Ref_count
_Ref_count_resource

这个图基本说明了他们之间的关系。shared_ptr、weak_ptr有一个共同基类_Ptr_base,其本身是一个类模板。同时,基类成员中涉及一个叫做_Ref_count_base的类。从名字上看,就知道这个类是用来做计数的,也就是shared_ptr关键的一点——引用计数。为了方便说明,先从后者开始讲解。

关于类_Ref_count_base的结构说明

这个计数类并不是很复杂。_Ref_count_base的声明如下:

class __declspec(novtable) _Ref_count_base;

该类加了一个宏__declspec(novtable),说明这个类是作为纯抽象类存在的,不应被实例化。
那么,首先谈论一下它的公有成员。

public member

第一点是构造函数与析构函数,还有运算符=的重载函数,定义如下:

_Ref_count_base(const _Ref_count_base&) = delete;
virtual ~_Ref_count_base() noexcept {}
_Ref_count_base& operator=(const _Ref_count_base&) = delete;

这里拷贝构造函数被禁用,其可被调用的构造函数被放置到protected中供子孙调用,稍后会谈到。析构函数被声明为virtual,与此前提到该类为抽象类相呼应,避免子类析构函数被屏蔽的问题出现。运算符=重载被禁用,在我看来是为了保证引用计数对象针对某个被智能指针管理的对象只能够存在一份。


第二点,则是关于引用计数加减问题的函数。代码很多,这里先列举声明:

bool _Incref_nz() noexcept;// 如果引用数非0就加1,增加成功返回true,否则为false
void _Incref() noexcept; // 引用数加1
void _Incwref() noexcept; // 弱引用数加1
void _Decref() noexcept; // 引用数减1
void _Decwref() noexcept; // 弱引用数减1
long _Use_count() const noexcept; // 返回引用数
virtual void* _Get_deleter(const type_info&) const noexcept; // 返回释放函数的指针

上述的函数注释里面,有几个函数的功能陈述是不全的。接下来,我将展开来谈谈。
在谈这些函数之前,必须先说说这几个函数里面用到的宏以及其展开后的内容:

// 展开前
_INTRIN_RELAXED(_InterlockedCompareExchange)
// 展开后
long _InterlockedCompareExchange(long volatile * _Destination, long _Exchange, long _Comparand);
// 展开前
#define _MT_INCR(x) _INTRIN_RELAXED(_InterlockedIncrement)(reinterpret_cast<volatile long*>(&x))
// 展开后
long _InterlockedIncrement(long volatile * _Addend);
// 展开前
#define _MT_DECR(x) _INTRIN_ACQ_REL(_InterlockedDecrement)(reinterpret_cast<volatile long*>(&x))
// 展开后
long _InterlockedDecrement(long volatile * _Addend);

其实这里写了很多废话,_INTRIN_RELAXED和_INTRIN_ACQ_REL的作用都是一样的,就是将宏替换为宏的参数而已,这么写只是为了做一些分类。这些宏展开后的函数,都是windows的SDK所提供的用于原子操作的函数。这意味着,其要求对内存某个地址的操作是互斥的。


memory order

讲到这里,就多提一嘴关于memory order的内容。刚才提到的两个宏,都是位于xatomic.h这个头文件中的,也就是说这些宏是与原子操作挂钩的。其中,_INTRIN是另外一个库,这个库中装载的是编译期实现的内置函数,是比较底层的内容(其实基本都能在windows的SDK文档里查看),宏的后半部分则是分别指代relaxed order以及acquire-release两种内存模式,这间接说明了内置函数的设计细节。
所谓内存模式的区分,即源于CPU的指令重排问题。比如很经典的例子:

int x=0,y=0;
// example 1
++x;
++y;
// example 2
++x;
y=x;

显然,案例1的两个语句互换顺序没有造成影响,而案例2却有很大问题。对于案例1,CPU可能会做指令重排处理——会有各种各样的理由,反正是CPU自己判断的——而案例2中CPU就不会再这么做了。有时候,我们需要人为地限制这些,所以就出现了##### memory order。
从宏展开后的内置函数名,可以猜到这些函数的功能大概是自增自减,所以这里就用自增做个例子。很简单的情况下,自增拆成三条汇编指令。假设我还是用个老机子16位的,寄存器AX,要自增的数据存放在内存1000h,那可能写出这样的东西:

MOV AX WORD PTR [1000] ; load
INC AX ; inc
MOV WORD PTR [1000] AX ; store

显然,这三个指令应当是被捆绑的。假设有线程1、2分别是t1、t2,前者负责写,后者负责读。定义这三条指令的行为是load、inc和store。那么,上述两种内存模式就可以被这样说明:

  • relaxed order只保证t1.store和t2.load是原子操作
  • acquire-release保证t1.store或t2.load前后的两部分指令不能互换。

回归正题,现在来讲一下上面几个宏展开之后的函数:

// 将从_Destination对应地址读出的long值与_Comparand比较,如果相等,则将该地址对应值替换为_Exchange,返回_Comparand
long _InterlockedCompareExchange(long volatile * _Destination, long _Exchange, long _Comparand);
// 将_Addend对应地址的long值加1
long _InterlockedIncrement(long volatile * _Addend);
// 将_Addend对应地址的long值减1
long _InterlockedDecrement(long volatile * _Addend);

讲完这些宏函数,就可以来讲讲类_Rep_count_base的成员函数了。在提到的这几个函数里面,_Incref_nz是最为复杂的了,就先来解决它:

bool _Incref_nz() noexcept { // increment use count if not zero, return true if successful
    auto& _Volatile_uses = reinterpret_cast<volatile long&>(_Uses);
#ifdef _M_CEE_PURE
    long _Count = *_Atomic_address_as<const long>(&_Volatile_uses);
#else
    long _Count = __iso_volatile_load32(reinterpret_cast<volatile int*>(&_Volatile_uses));
#endif
    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;
}

这个代码里面还有个_M_CEE_PURE的宏,这个是微软的公共语言运行时/clr:pure的相关宏,关系不大,先不提及。源码中并没有定义这个宏,所以执行的是#else部分的代码。这个函数的作用是让引用数_Uses自增1,当且仅当_Uses不为0。在之前的类图里面,说到该类有两个私有成员:

typedef _Atomic_counter_t unsigned long;
_Atomic_counter_t _Uses = 1;
_Atomic_counter_t _Weaks = 1;

代码采用reinterpret_cast进行了强制转换,这个用的非常好。因为这个转换是不会做安全检查的,安全性由程序员来保证,所以转换的性能非常高。由于本身就保证了_Uses的非负,所以这个转换是能保证正确的。之后,又将这个值的地址从long指针转换为int指针作为参数传入。这里分了两步,主要是为了适配clr,其参数是volatile的,实际上如果没有这个宏,一步转到int*也是没有任何影响的。
__iso_volatile_load32是个内部函数,就是从传入的地址中读个32位的数出来。之后,便是一个循环的访存。该循环不允许引用为0的时候做自增。由于提到这是relaxed方式,也就是说只有store和load两个操作互斥。在__iso_volatile_load32这个函数读出数值之后,很可能有新值被写入,所以必须再进行一个比较。如果发现“旧值”与内存的值不匹配,就说明有新的数据被写入,就需要更新“旧值”,再进行新的判断,直到匹配完成才返回true。如果没有成功自加,就返回false。
关于自增的函数还有两个:

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

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

第一个函数是对_Uses自增,第二个则是对_Weaks自增。_Weaks是weak_ptr的引用计数,所以说weak_ptr不会增加引用计数是个不全面的说法,应该说是不会增加_Uses的值才对。这里自增调用的内置函数,是没有检查的,也就是说无论内存中的值是否正确,都会直接加上。
关于自减的函数有两个:

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

同样的,第一个函数是对_Uses自减,第二个是对_Weaks自减。不过,显然的,这次的函数就没有自增那么简单了。当_Uses自减后值为0,就会调用两个函数。_Destroy位于private成员区域,稍后再提及。而_Decwref则是再判断弱引用_Weaks的数量自减之后是否为0,如果是0就调用_Delete_this,这个函数也是private的。
非常巧妙的一点是,两个计数器的初始值都是1。也就是说,实际上_Weaks的值是比实际的weak_ptr是要多1的。但这无伤大雅,因为我们几乎不会去找这个值到底是什么,而这样设计使得函数结构更加简洁。


除去上述说到的函数,该类的公有函数还有以下两个:

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

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

第一个函数显然的就是返回_Uses的值。第二个函数看似无用,但由于它是virtual,再加上之前提到它的声明中带有__declspec(novtable)的宏,我们可以猜想它的子类一定有其它实现。

protected member

protected区域只有一个函数,就是无参的构造函数。这个函数将会被其子类调用,设置为protected是为了不让外部构造其实例。

private member

private成员一共四个,除去之前提及的两种引用计数外,还包含两个虚函数:

#ifdef _M_CEE_PURE
// permanent workaround to avoid mentioning _purecall in msvcurt.lib, ptrustu.lib, or other support libs
virtual void _Destroy() noexcept {
    _STD terminate();
}

virtual void _Delete_this() noexcept {
    _STD terminate();
}
#else // ^^^ _M_CEE_PURE / !_M_CEE_PURE vvv
virtual void _Destroy() noexcept     = 0; // destroy managed resource
virtual void _Delete_this() noexcept = 0; // destroy self
#endif // _M_CEE_PURE

clr的宏先不看,这两个函数都是我们刚才提及过的,它们在非clr情境下是没有具体实现的。
至此,_Rep_count_base类就解析完成了。接下来,我将再谈谈它的其中一个子类_Rep_count。

关于类_Rep_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:
    void _Destroy() noexcept override { // destroy managed resource
        delete _Ptr;
    }
    void _Delete_this() noexcept override { // destroy self
        delete this;
    }
    _Ty* _Ptr;
};

这里,我们就看到了两个虚函数的重写内容。_Destroy函数是删除了指针指向的内容——也就是被share的那个对象——而_Delete_this则是将自己给删除了——也就是清除了计数使用的这个辅助类。
接下来,我将再谈谈它的另一个子类_Ref_count_resource。

关于类_Ref_count_resource的结构说明

这个类有一些特别的地方需要说明,分别是三个函数与一个成员变量:

public:
	void* _Get_deleter(const type_info& _Typeid) const noexcept override {
#if _HAS_STATIC_RTTI
        if (_Typeid == typeid(_Dx)) {
            return const_cast<_Dx*>(_STD addressof(_Mypair._Get_first()));
        }
#else // _HAS_STATIC_RTTI
        (void) _Typeid;
#endif // _HAS_STATIC_RTTI
        return nullptr;
    }
private:
    void _Destroy() noexcept override { // destroy managed resource
        _Mypair._Get_first()(_Mypair._Myval2);
    }
    void _Delete_this() noexcept override { // destroy self
        delete this;
    }
    _Compressed_pair<_Dx, _Resource> _Mypair;

可以看到,在这个类里,基类的_Get_deleter方法被重写了。这里定义了一个宏,也就是存在RTTI的时候,这个函数才会真正的起到作用。这里的_Dx实际上是一个函数,也就是所谓的删除器deleter。函数采用了RTTI的type_info对对象的基本属性进行了比对,并返回这个deleter。
众所周知,智能指针是能够自己定义deleter的,可以自定义智能指针生命周期结束后对管理的堆对象的操作。_Resource就是指向堆对象的普通指针,_Mypair是一个哈希表,用作deleter和resource的键值对管理。这里,_Destroy函数与_Rep_count类所做的工作不一样,不是直接释放堆内存,而是调用了deleter去处理这个内存块。

计数类的内容就到这里结束了,接下来将会开始重头戏。

关于基类_Ptr_base的结构说明

_Ptr_base本身是一个类模板,模板参数个数为一个。这个类的结构是非常复杂的,有众多的成员,一个个分析内容将冗长,且无意义——因为工作原理是相似的——因此只选择关键的地方做说明。
这个类的声明是这样的:

template<class _Ty>
class _Ptr_base;

与前文不同,为了方便,我首先说明它的private成员。

private member

private的有关内容非常多,但不是每个都需要说明。首先,要提及的内容是两个指针:

element_type* _Ptr{nullptr};
_Ref_count_base* _Rep{nullptr};

_Ref_count_base我想不需要再说明了,这个element_type是啥呢?在public区域有这样一个定义:

using element_type = remove_extent_t<_Ty>;

remove_extent_t是位于memory库里面的用于SFINAE的结构体,功能是降维,也就是说三维数组降成二维,以此类推,当数组维度为0时不再下降,而是返回其本身。这个性质可以很好地与“指针”这一工具结合。

public member

公有成员中首先提及的,是类构造函数及其运算符=重载函数,其结构是这样的:

_Ptr_base(const _Ptr_base&) = delete;
_Ptr_base& operator=(const _Ptr_base&) = delete;

显然,该类不允许外部实例化其对象,但是却把这个机会留给了它的子孙——这一点在稍后会提及。同时,该类对象——虽然并不能存在——也不能进行浅复制。
剩余的还有两个函数,其表示如下:

_NODISCARD long use_count() const noexcept {
    return _Rep ? _Rep->_Use_count() : 0;
}
template <class _Ty2>
_NODISCARD bool owner_before(const _Ptr_base<_Ty2>& _Right) const noexcept { // compare addresses of manager objects
    return _Rep < _Right._Rep;
}

_NODISCARD就是C++17的注解[[nodiscard]],让编译器警告不接收返回值的行为。第一个函数很简单,就是调用了_Rep_count_base的_Use_count函数。如果此时这个类对象还没被实例化,那就返回0。第二个函数owner_before是用来解决一个问题,就是一个指针指向某个堆对象,而另一个指针只指向这个堆对象的一部分。在结束生命周期后,能够保证不会出现多次析构的情况。相关情况会在后续提及。

protected member

protected的函数就很多了。构造与析构函数就没必要再讲,毕竟是default的。要讲的第一个函数是这个:

_NODISCARD element_type* get() const noexcept {
    return _Ptr;
}

这是获取了指向管理的堆对象的指针。
此外,该区域有大量的与构造相关的函数,我挑选一些比较关键的内容来谈谈:

// 移动构造,就是将_Right管理的指针置空,把对象交付给this管理
	template <class _Ty2>
    void _Move_construct_from(_Ptr_base<_Ty2>&& _Right) noexcept {
        _Ptr = _Right._Ptr;
        _Rep = _Right._Rep;

        _Right._Ptr = nullptr;
        _Right._Rep = nullptr;
    }
// 复制构造就是直接引用数加一,同时_Rep_count_base指针也同步
    template <class _Ty2>
    void _Copy_construct_from(const shared_ptr<_Ty2>& _Other) noexcept {
        _Other._Incref();

        _Ptr = _Other._Ptr;
        _Rep = _Other._Rep;
    }
// 别名构造,比较特别,是与上文讲到的owner_before相关的。当_Other管理的是this管理对象的一部分,
// 就认为有另一个指针在引用这个对象,所以要将引用数加1,
// 并同步_Rep_count_base指针。
// 当然,如果非要让这个_Px指向其它数据,那我也没办法。
    template <class _Ty2>
    void _Alias_construct_from(const shared_ptr<_Ty2>& _Other, element_type* _Px) noexcept {
        _Other._Incref();

        _Ptr = _Px;
        _Rep = _Other._Rep;
    }
// 别名移动构造,就是移动构造和别名构造的结合罢了
    template <class _Ty2>
    void _Alias_move_construct_from(shared_ptr<_Ty2>&& _Other, element_type* _Px) noexcept {
        _Ptr = _Px;
        _Rep = _Other._Rep;

        _Other._Ptr = nullptr;
        _Other._Rep = nullptr;
    }
// 通过weak_ptr构造,得先保证引用数已经不是0了,否则是无效的
    template <class _Ty2>
    bool _Construct_from_weak(const weak_ptr<_Ty2>& _Other) noexcept {
        if (_Other._Rep && _Other._Rep->_Incref_nz()) {
            _Ptr = _Other._Ptr;
            _Rep = _Other._Rep;
            return true;
        }

        return false;
    }

除去这些函数,还有自增、自减、交换指针等等函数,这些比较简单就不用说明了。
至此,基类的说明就完毕了。接下来就是主角——shared_ptr登场了。

关于主角shared_ptr的说明

前面铺垫了这么久,其实甚至已经能猜到shared_ptr的实现方式了,shared_ptr的结构其实不需要再多说,因此我将结合实际的案例来讲解。

案例一

假如有这样的代码:

#include<memory>
using namespace std;
int main(){
    shared_ptr<int> p(new int(4));
    return 0;
}

首先,shared_ptr会匹配到它的其中一个构造函数,其形式是这样的:

template <class _Ux,
        enable_if_t<conjunction_v<conditional_t<is_array_v<_Ty>, _Can_array_delete<_Ux>, _Can_scalar_delete<_Ux>>,
                        _SP_convertible<_Ux, _Ty>>,
            int> = 0>
explicit shared_ptr(_Ux* _Px) {
    if constexpr (is_array_v<_Ty>) {
        _Setpd(_Px, default_delete<_Ux[]>{});
    } else {
        _Temporary_owner<_Ux> _Owner(_Px);
        _Set_ptr_rep_and_enable_shared(_Owner._Ptr, new _Ref_count<_Ux>(_Owner._Ptr));
        _Owner._Ptr = nullptr;
    }
}

这个构造函数是真的长啊,要一点点去分析讲了什么。
首先,看到模板,很快意识到这里用了SFINAE的技巧。因此,需要先插入一个题外话,讲讲这里用到的SFINAE。


conjunction_v

这个东西在文件type_traits里面,相关的定义是这样的:

template <bool _First_value, class _First, class... _Rest>
struct _Conjunction {
    using type = _First;
};

template <class _True, class _Next, class... _Rest>
struct _Conjunction<true, _True, _Next, _Rest...> {
    using type = typename _Conjunction<_Next::value, _Next, _Rest...>::type;
};

template <class... _Traits>
struct conjunction : true_type {};

template <class _First, class... _Rest>
struct conjunction<_First, _Rest...> : _Conjunction<_First::value, _First, _Rest...>::type {};

template <class... _Traits>
_INLINE_VAR constexpr bool conjunction_v = conjunction<_Traits...>::value;

_Conjunction做了模板特化,功能是递归对它不定长个模板参数的成员value进行逻辑判定,一直到出现false的时候才停下来,并保存这个false值对应的模板参数类型。
conjunction也做了模板特化,当不再有模板参数时,conjunction内含一个true值,否则就是递归判断模板参数的value。
conjunction_v则是conjunction的value成员值,这也要求_Conjunction的模板参数类型是必须带有value成员的。

conditional_t

这个东西的相关定义是这样的:

template <bool _Test, class _Ty1, class _Ty2>
struct conditional { 
    using type = _Ty1;
};

template <class _Ty1, class _Ty2>
struct conditional<false, _Ty1, _Ty2> {
    using type = _Ty2;
};

template <bool _Test, class _Ty1, class _Ty2>
using conditional_t = typename conditional<_Test, _Ty1, _Ty2>::type;

简单来说,就是当bool为true,就记录第二个参数的类型,否则记录第三个参数的类型。

_Can_array_delete和_Can_scalar_delete

这两个就不给定义了,其实就是判断是是否定义了delete这个行为罢了。

_SP_convertible

就是判断后面两个参数类型是不是能互转,比较简单。


解释完毕之后,再来看回源码。这个SFINAE无非限制了这些情况,即类模板参数_Ty是数组还是非数组,并根据这个要求其定义了delete行为。此外,它还要求参数_Ux和_Ty是可转换的。只要满足这些条件,SFINAE就通过了。
进入函数体内,第一个是编译期的判断,这里先不讨论参数类型是数组的情况。进入else分支,这里提及了一个类叫做_Temporary_owner,顾名思义就是暂时拥有者。之后调用了shared_ptr的一个private函数,再把暂时拥有者占用的指针置空。
_Set_ptr_rep_and_enable_shared函数有两个重载,分别对应了空指针和非空指针的情况:

template <class _Ux>
void _Set_ptr_rep_and_enable_shared(_Ux* const _Px, _Ref_count_base* const _Rx) noexcept {
    this->_Ptr = _Px;
    this->_Rep = _Rx;
    if constexpr (conjunction_v<negation<is_array<_Ty>>, negation<is_volatile<_Ux>>, _Can_enable_shared<_Ux>>) {
        if (_Px && _Px->_Wptr.expired()) {
            _Px->_Wptr = shared_ptr<remove_cv_t<_Ux>>(*this, const_cast<remove_cv_t<_Ux>*>(_Px));
        }
    }
}

void _Set_ptr_rep_and_enable_shared(nullptr_t, _Ref_count_base* const _Rx) noexcept {
    this->_Ptr = nullptr;
    this->_Rep = _Rx;
}

这个函数内容比较简单,其实就是去指向这个新创建的计数辅助类以及堆对象。至于编译期判断的部分,实际上是与weak_ptr有关——它显然调用了expired函数。其中,_Can_enable_shared涉及到了另一个类enable_shared_from_this,这更多涉及到weak_ptr的内容,就先不细说了。
该函数完结后,构造函数就结束了。shared_ptr的析构函数很简单:

~shared_ptr() noexcept { // release resource
    this->_Decref();
}

_Decref我们提过,就是嵌套调用的_Rep的_Decref做计数自减,并且判断是否应当销毁。
至此,案例一结束。

案例二

假如有这样的代码:

#include<memory>
using namespace std;

void deleter(int* ptr){delete ptr;}

int main(){
    shared_ptr<int> p(new int, deleter);
    return 0;
}

这次调用的构造函数是这样子的:

template <class _Ux, class _Dx,
        enable_if_t<conjunction_v<is_move_constructible<_Dx>, _Can_call_function_object<_Dx&, _Ux*&>,
                        _SP_convertible<_Ux, _Ty>>,
            int> = 0>
shared_ptr(_Ux* _Px, _Dx _Dt) { 
    _Setpd(_Px, _STD move(_Dt));
}

这里又多了个叫做_Can_call_function_object的东西,实际上就是判断_Dx这个函数是否接受的是_Ux作为参数而已。is_move_constructible就是看是否支持移动语义——因为后面用到了。函数体内调用了新的函数_Setpd,其内容如下:

template <class _UxptrOrNullptr, class _Dx>
void _Setpd(const _UxptrOrNullptr _Px, _Dx _Dt) {
    _Temporary_owner_del<_UxptrOrNullptr, _Dx> _Owner(_Px, _Dt);
    _Set_ptr_rep_and_enable_shared(
        _Owner._Ptr, new _Ref_count_resource<_UxptrOrNullptr, _Dx>(_Owner._Ptr, _STD move(_Dt)));
    _Owner._Call_deleter = false;
}

这里其实也是差不多的,最大的区别就是计数类从_Ref_count换成了_Ref_count_resource,因为后者才有get_deleter函数的重写,也有管理deleter的功能。
析构函数不提,一致。
至此,案例二也结束了。

案例三

假如有这样的代码:

#include<memory>
using namespace std;

struct A{
    int aa;
    double aaa;
};

int main(){
    shared_ptr<A> p(new A);
    shared_ptr<int> pp(p, &p->aa);
    return 0;
}

这次调用的构造函数是这样子的:

template <class _Ty2>
shared_ptr(const shared_ptr<_Ty2>& _Right, element_type* _Px) noexcept {
    this->_Alias_construct_from(_Right, _Px);
}

这里的别名构造刚才已经提过,比较简单。
析构函数也是一致的,不提。
至此,案例三也结束了。


总结

大致分析了一下shared_ptr的工作原理,涉及的类还是比较多的,也关系到一些SFINAE的知识。

一些思考

源码中的一些部分引起了我的思考:

  • _Rep_count类中大量使用了虚函数,这自然会带来RTTI。虽然采用这种方式,很有效地对其两个子类进行了抽象,但RTTI不可避免的会带来性能上的损耗。作为一个库函数,这是否是可以接受的呢?是否能够存在一个更加好的解决方法来避免呢?
  • _Get_deleter函数对函数的类型信息又做了一次判断,但实际上这在构造函数时就已经分析过,是否需要再进行一次呢?
  • 自增和读取采用了relaxed,而自减却用了acquire-release,这是否是因为自减操作关联了销毁,而需要确保其指令顺序不被重排呢?
  • 关于owner_before,确实有个不好的地方,就是stored指针中我所管理的对象与我传入的值可以不在同一区间。非常遗憾的是,即便sizeof是编译期的,指针的地址要在程序函数栈创建后才能确定,而堆对象的地址更要分配之后才能得知,这使得堆对象所处的内存区块不能在编译期被确认,也就不能采用SFINAE的方式进行限制了。虽然如此,但是否应该采用assert这样的函数去打断言呢?
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值