造shared_ptr轮子时需要注意的几点

最近在写智能指针shared_ptr的实现,本以为是一个比较简单的轮子,但其实内部实现是比较复杂的,总的来说总共有以下几项。

内部数据结构的实现

shared_ptr有两个私有成员,一个为指向对象指针,另一个为引用计数块的指针。

我刚开始以为内部只是一个指向对象指针和一个引用计数(比如size_t)的指针,其实内部构造并不是这么简单,智能指针shared_ptr需要满足下面几点。

  • 存储的指针类型不一定与被管理的指针相同

  • shared_ptr要满足多态的要求

  • 可以自定义删除器

  • 引用计数变化时要求为原子操作

上面第一点说的是这个构造函数

template< class Y > 
shared_ptr( const shared_ptr<Y>& r, element_type* ptr ) noexcept;

这个构造函数的测试代码如下(来源于 cppreference )

#include <iostream>
#include <memory>

struct Foo {
    int n1;
    int n2; 
    Foo(int a, int b) : n1(a), n2(b) {}
};
int main()
{   
    auto p1 = std::make_shared<Foo>(1, 2);
    std::shared_ptr<int> p2(p1, &p1->n1);
    std::shared_ptr<int> p3(p1, &p1->n2);

    std::cout << std::boolalpha
              << "p2 < p3 " << (p2 < p3) << '\n'
              << "p3 < p2 " << (p3 < p2) << '\n'
              << "p2.owner_before(p3) " << p2.owner_before(p3) << '\n'
              << "p3.owner_before(p2) " << p3.owner_before(p2) << '\n';

    std::weak_ptr<int> w2(p2);
    std::weak_ptr<int> w3(p3);
    std::cout 
//              << "w2 < w3 " << (w2 < w3) << '\n'  // won't compile 
//              << "w3 < w2 " << (w3 < w2) << '\n'  // won't compile
              << "w2.owner_before(w3) " << w2.owner_before(w3) << '\n'
              << "w3.owner_before(w2) " << w3.owner_before(w2) << '\n';

}

这说明了内部指针与管理的指针并不需要一致,再加上多态的要求,我们需要引用计数块的模板类型有指针类型也要有删除器类型,并且模板类型不同时父类与子类是无法相互转化的,所以我们必须首先声明一个抽象基类。

抽象基类中有两个数据成员,这里值得一提的是shared_ptr与weak_ptr是有联系的,典型的实现为创建shared_ptr时将强计数与弱计数都置为1,然后shared_ptr执行析构函数时将强计数减一,如果减至0,则用删除器删除指针,然后将弱计数减一,如果此时弱计数为0,则销毁引用块,如果不为0,等到最后一个weak_ptr执行析构函数时再进行销毁引用块。

上面说的实现是不用make_shared()函数进行产生智能指针的情况,因为如果用到了make_shared()函数产生智能指针的话是仅进行一次分配的,这里就不细谈了。

然后我们关注下面的两个私有成员变量,两个私有成员变量均为原子类,这保证了多线程时引用计数的线程安全性。

class ref_count_base {
private:
    std::atomic<size_t>shared_cnt;
    std::atomic<size_t>weak_cnt;
}

然后其他的公有函数为操作内部计数的接口,这里就不讲了,但是有其他的虚函数需要讲一下。

virtual void* get_deleter() = 0;
//用于得到删除器,以便非成员函数get_deleter()获取
virtual void release() = 0;
//释放资源,也就是进行销毁引用对象
virtual void destory() = 0;
//删除本身,也就是 delete this,为什么这么做呢?因为这样方便我们进行跟踪变化

随后便是继承这个基类,进行内部的实现

template<typename T, typename Deleter = default_delete<T>>
struct ref_count :public ref_count_base;

然后我们在shared_ptr类模板中新增 ref_count_base 指针便可以指向任意引用快,这样便可以实现多态了。

管理动态数组

C++中有new[]可以创建动态数组,然而返回的指针只是一个指针,并不具有数组的形式,所以如果我们直接传入指针进行便不能直接进行管理,需要自己传入删除器才能进行资源管理,这样无疑增加了麻烦。

实际上C++11标准规定的shared_ptr并不具备管理动态数组的功能,但是这个实现其实是不难的,我们有两个方案实现这个功能。

第一个方案为先创建一个__shared_ptr的基类,然后shared_ptr继承这个基类,然后再用特化版本对数组版本进行特化,这样是比较麻烦的,又因为一大堆构造函数与赋值函需要重写进行转发,这样明显是比较麻烦的,所以我们采用第二种方案。

说第二种方案前我们需要知道为什么不能直接使用数组版本进行管理,这是因为我们传入的模板类型T[]然后内部指针存储的其实为 T[]* 这样显然不是我们想要的。

经过查询资料发现,我们可以用<type_traits>头文件中的std::remove_extent_t进行数组类型拆除,具体实现如下

using element_type = std::remove_extent_t<T>;

我们这样就可以得到T类型了,这样就可以直接使用数组类了,然后我们new引用计数块的时候需要这么做

template<typename Y>
explicit shared_ptr(Y* p)
    : ptr(p), ref(new ref_block<std::conditional_t<std::is_array_v<T>,Y[],Y>>(p)) {}

才能使传入的Y*类型的指针创建适合自己版本的引用计数块

enable_shared_from_this

enable_shared_from_this类能使一个对象通过调用自己的成员函数获得管理的shared_ptr的副本,我们不能通过 shared_ptr\(this) 的方式获得shared_ptr,因为这样的话会导致析构两次,所以我们需要在第一次创建shared_ptr时就进行相关的操作。

典型的实现为enable_shared_from_this类存储weak_ptr,然后某个类直接继承这个类,在shared_ptr的构造函数中将shared_ptr构造weak_ptr,这个思路很好理解,但是我们如何通过传入的类型来得知其是否继承了enable_shared_from_this类呢?

答案便是继续利用\<type_traits>的工具,这里用到了std::enable_if以及std::is_base_of

    template<typename Y, std::enable_if_t<!std::is_base_of_v<enable_shared_from_this<Y>, Y>, int> = 0>
    void _shared_from_this(Y* p) {}

    template<typename Y, std::enable_if_t<std::is_base_of_v<enable_shared_from_this<Y>,Y>,int> = 0>
        void _shared_from_this(Y* p) {
            p->_internal_accept_owner(*this);
    }