Loading

C++中的智能指针

引言

普通指针使用时存在挂起引用以及内存泄漏的问题,C++ 11中引入了智能指针来解决它

挂起引用:当多个指针指向一个内存时,其中一个指针释放后其他指针依旧执行释放操作,就发生了挂起引用

内存泄漏:在堆区开辟空间后没有在使用结束时将其释放,即new后无delete

std::unique_ptr

std::auto_ptr,时代的眼泪

std::unique_ptrstd::auto_ptr的替代品,解决了C++ 11之前std::auto_ptr的很多缺漏

简单的看一下std::auto_ptr的复制构造函数

template<typename T>
class auto_ptr {
public:
    // Codes..
    auto_ptr(auto_ptr& atp) {
    	m_ptr = atp.m_ptr;
    	atp.m_ptr = nullptr; 
    }
private:
    T* m_ptr;
};

可以很容易的看出,该函数将指针所有权从一个对象转移到另外一个对象,且将原对象置空。该函数中std::auto_ptr实际上在运用引用去实现移动语义。但若是在转移所有权后仍去访问前一个对象(现在已经被置为空指针),程序会崩溃。

std::auto_ptr<int> atp(new int(10));
std::auto_ptr<int> atp2(atp);
// auto _data = atp.data;		// undefined behavior
// 此时的atp已经为nullptr,因为“移动函数”将所有权转接给了另外一个对象

u_ptr的移动构造

在C++ 11中引入了右值引用,可在此基础上实现移动构造函数。如今std::auto_ptr在C++17中被移除,正式被std::unique_ptr替代

// unique_ptr的移动构造函数
unique_ptr(unique_ptr&& unip) : m_ptr(unip.m_ptr)
{
	unip.m_ptr = nullptr;
}

虽然说这个函数与std::auto_ptr中的做法一样,但它只能接受右值作为参数。传递右值,即为转换指针所有权

std::unique_ptr<int> a(new int(10));
// std::unique_ptr<int> b(a);	// error
std::unique_ptr<int> b(std::move(a));	// ok

u_ptr的构造方式

std::shared_ptr不同,该智能指针用于不能被多个实例共享的内存管理,也就是说,仅有一个实例拥有内存所有权

对于智能指针而言,声明后会默认置为空

std::unique_ptr<int> unip1;	// 默认置nullptr
std::unique_ptr<int> unip2(new int(5));	// 旧方法
std::unique_ptr<int> unip3 = std::make_unique<int>(10);	// 新规范
// std::unique_ptr<int> unip3 = std::make_unique<int>(new int(10));	// 迷惑操作

std::unique_ptr<std::string> unip4 = std::make_unique<std::string>("Pointer");

std::array<int, 10> arr = { 1,2,3,4,5,6,7,8,9,0 };
std::unique_ptr<std::array<int, 10>> unip5 = std::make_unique<std::array<int, 10>>(arr);

需要注意的是,接受指针参数的ctorexplicit的,因此我们不能将一个内置指针隐式转换为一个智能指针

template <class _Dx2 = _Dx, _Unique_ptr_enable_default_t<_Dx2> = 0>
explicit unique_ptr(pointer _Ptr) noexcept : _Mypair(_Zero_then_variadic_args_t{}, _Ptr) {}

// std::unique_ptr<int> unip = new int(10); 	// 错误

u_ptr的unique性

std::unique_ptr通过删除拷贝构造函数和拷贝赋值函数来确保unique性,它是move-only的,独占资源

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

可以通过移动语义转移所有权

std::unique_ptr<int> unip = std::make_unique<int>(10);
// std::unique_ptr<int> copy_unip = unip;
// std::unique_ptr<int> copy_unip(unip);

// 移动构造 unip被置空,所有权转到move_unip上
std::unique_ptr<int> move_unip = std::move(unip);

但我们可以“拷贝或是赋值”一份即将销毁的std::unique_ptr,最常见的例子是从函数返回一个std::unique_ptr

std::unique_ptr<int> clone() 
{
    std::unique_ptr<int> unip = std::make_unique<int>(10);
    // 返回值实际上调用到移动构造
    return unip;
}
// 编译器优化为一次移动构造
std::unique_ptr<int> p1 = clone();

std::unique_ptr<int> p2;
// p2调用一次移动赋值 clone调用一次移动构造
p2 = clone();

std::unique_ptr对象可以传给左值常量引用常数,因为这样并不会改变内存所有权,也可以使用移动语义进行右值传值(注意区分转移所有权和std::move的区别)

class Test {};
void right_param(std::unique_ptr<Test>&& t) {}
void cref_param(const std::unique_ptr<Test>& t) {}

int main()
{
    auto temp = std::make_unique<Test>();
    
    // 错误 无法将右值引用绑定到左值
    // right_param(temp);

    // 利用std::move强转为右值类型
    right_param(std::move(temp));
    cref_param(std::move(temp));
    
    cref_param(temp);
}

u_ptr构造的坑点

对于该智能指针来说,应避免犯以下两个错误(std::unique_ptrstd::shared_ptr同理)

  • 用同一份指针初始化多个智能指针

    int *p = new int(10);
    std::unique_ptr<int> unip(p);
    std::unique_ptr<int> unip2(p);
    

    拯救方法,释放其中一个智能指针的管理权

    unip.release();
    
  • 混用普通指针与智能指针

    int *p = new int(10);
    std::unique_ptr<int> unip(p);
    delete p;
    // 离开作用域后unip将会释放一块已经释放的内存
    

u_ptr的主动弃权与释放

std::unique_ptr中有get()release()reset()方法

  • get()可以获得智能指针中管理的资源,返回的是指针
  • release()会返回指向所管理的资源的指针,并释放其管理权(并不会析构
  • reset()会调用构造函数并回收内存,同时reset()还可以接受一个内置指针
std::unique_ptr<float> unip = std::make_unique<float>(10);
std::unique_ptr<float> unip2 = std::make_unique<float>(20);

// unip.reset(unip2.get())		// 绝对禁止的操作 会导致挂起引用
unip.reset(unip2.release());	// unip析构其管理的内存后 接收unip2转接过来的所有权

// unip = std::move(unip2);		// 与reset-release相同

u_ptr的size

std::unique_ptr使用默认删除器的时候,它的大小与原始指针的大小相同。当用户传入自定义删除器时,std::unique_ptr的大小需要分情况讨论

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

int main() {
    // 32位环境下 uniquePtr1的大小是8
    std::unique_ptr<int, void(*)(int*)> uniquePtr1(new int(10), deleter);
    
    // 32位环境下 uniquePtr2的大小是48 函数对象本身的大小是40
    std::unique_ptr<int, std::function<void(int*)>> uniquePtr2(new int(10), deleter);
    
    // 32位环境下 uniquePtr3的大小是4
    auto lambda = [](int* p) { delete p; };
    std::unique_ptr<int, decltype(lambda)> uniquePtr3(new int(10), lambda);
}

综上所述,无状态的函数对象(例如无捕获的lambda表达式)的存储大小是最优的。并且若std::unique_ptr中使用了自定义删除器,即使它们管理的是同类型的数据,它们本身也是不同的类型

std::shared_ptr

std::shared_ptrstd::unique_ptr的主要区别在于前者是使用引用计数的智能指针,后者是独占资源的智能指针。强引用计数维护了“引用同一个真实指针对象的智能指针实例”的数目。这意味着,可以有多个std::shared_ptr实例可以指向同一块动态分配的资源块和控制块

s_ptr的构造

std::unique_ptr不同,std::shared_ptr支持拷贝构造,也允许拷贝赋值

std::shared_ptr<int> shrp = std::make_shared<int>(10);
std::shared_ptr<int> shrp2 = shrp;
std::shared_ptr<int> shrp3(shrp2);
std::cout << shrp.use_count() << std::endl;			// 3

更重要的是,std::shared_ptr可以由std::unique_ptr移动而来(或从返回std::unique_ptr的函数构建,本质上也是移动)

auto lambda = [](int* p) { delete p; };
std::unique_ptr<int, std::function<void(int*)>> uniquePtr(new int(10), lambda);
// 构建sharedPtr
std::shared_ptr<int> sharedPtr = std::move(uniquePtr);

s_ptr的应用场景

假设第三方库有这么一些代码

struct IntData {
    int data = 100;
    ~IntData() { std::cout << "int data destroy" << std::endl; }
};

class OtherLibHandler {
public:
    template<typename T>
    void* Create() { return new T(); }

    template<typename T>
    void Release(void *p) { delete reinterpret_cast<T*>(p); }
};

static OtherLibHandler& GetHandle() {
    static OtherLibHandler p;
    return p;
}

第三方库通常会通过接口提供原始指针,用来管理其中的内存,既然是原始指针,就总会出现忘记使用库中的释放函数,导致内存泄露的情况

void* p = GetHandle().Create<IntData>();
// Codes..
// GetHandle().Release<IntData>(p);			//程序员由于不可抗力导致漏写这行代码了

那么使用智能指针去维护就会显得非常方便,以下仅提供代码思路

auto deleter = [](void* p) { GetHandle().Release<IntData>(p); };
std::shared_ptr<void> shrp(GetHandle().Create<IntData>(), deleter);

包装成一个函数

template<typename T>
std::shared_ptr<T> GetShared(void* p) {
    std::shared_ptr<T> shrp(reinterpret_cast<T*>(p), [](void* p) { GetHandle().Release<T>(p); });
    return shrp;
}

int main() {
    std::shared_ptr<Intdata> shrp = GetShared<IntData>(GetHandle().Create<IntData>());
}

但是包装成函数后会有这么一个问题,GetShared返回出来的智能指针必须得被接下来(不管是左值或是右值引用,或是常量左值引用),否则该对象将会被释放,导致后续代码去访问野指针的内容

void* p = GetHandle().Create<IntData>();
GetShared<IntData>(p);
// 出现不确定的结果
std::cout << reinterpret_cast<IntData*>(p)->data << std::endl;

那我们可以使用宏来确保一定创建一个对象来接住返回值

#define CreateShared_IntData(p) auto shared_##p = GetShared<IntData>(p);

template<typename T>
std::shared_ptr<T> GetShared(void* p) {
    std::shared_ptr<T> shrp(reinterpret_cast<T*>(p), [](void* p) { GetHandle().Release<T>(p); });
    return shrp;
}

int main() {
    auto origin_p = GetHandle().Create<IntData>();
    CreateShared_IntData(origin_p);
    // 100
    std::cout << shared_origin_p->data << std::endl;
}

s_ptr的API

  • 使用use_count()来查看std::shared_ptr的强引用次数
  • reset()则可以释放关联内存块的所有权(相当于std::unique_ptrrelease(),但它没有返回值 ),如果是最后一个指向该资源的std::shared_ptr,就释放这块内存。同时reset()还可以传入一个新的原始指针,来让智能指针管理新的内存
  • 三种智能指针都有swap(),用于交换两个同种类型的指针对象

s_ptr的坑点

上文中提到过我们应该避免用同一份指针初始化多个智能指针

int *p = new int(10);
std::shared_ptr<int> shrp(p);
std::shared_ptr<int> shrp2(p);
// 导致同一片地址的资源被释放两次

因为这两个智能指针是独立初始化的,所以它们之间并没有通讯共享引用计数。std::shared_ptr的内部实际上维护两个指针,一个用于管理实际的资源,另外一个则指向一个控制块,其中记录了有哪些std::shared_ptr共同管理同一片内存。

// std::shared_ptr的基类_Ptr_base维护了两个指针
private:
    element_type* _Ptr{nullptr};
    _Ref_count_base* _Rep{nullptr};

这是在初始化完成的,所以如果单独初始化两个对象,尽管管理的是同一块内存,它们各自的”控制块“没有互相记录的。但是如果是使用复制构造函数还有赋值运算时,控制块将会同步更新,这样就达到了引用计数的目的。

s_ptr的引用次数

std::shared_ptr中采用原子操作进行引用计数的增加和减少,因此它在多线程环境下依旧能正常工作

并且s_ptr的引用次数在移动操作中不会增加或者减少。对于移动操作来说,代表了一个std::shared_ptr的消散和一个std::shared_ptr的诞生,结果就是不需要进行任何引用计数的操作。因此std::shared_ptr的移动操作会比复制操作更快

s_ptr的size

std::shared_ptr的大小是普通指针的两倍,因为他内部维护了两个指针分别指向资源块和控制块。

std::unique_ptr不同,std::shared_ptr自定义删除器不会改变智能指针的类型,这意味着提供不同种类删除器的std::shared_ptr能够被放到同一个容器中,如

std::vector<std::shared_ptr<int>> vec;

并且为std::shared_ptr提供删除器不会改变智能指针本身的大小,std::shared_ptr会对删除器(或内存分配器)做出一份拷贝,然后存放在控制块上(在堆上),由指向控制块的指针管理

死锁与自锁

如果两个强引用类型的智能指针互相引用会如何

//照搬知乎上的代码
class Person
{
public:
	Person(std::string name) :
		m_name(std::move(name)), m_partner(nullptr)
	{
		std::cout << m_name << " created" << std::endl;
	}

    ~Person()
    {
        std::cout << m_name << " destoryed" << std::endl;
    }

    friend void partnerUp(std::shared_ptr<Person>& shrp1, std::shared_ptr<Person>& shrp2)
    {
        if (!shrp1 || !shrp2)
            return;
        shrp1->m_partner = shrp2;
        shrp2->m_partner = shrp1;
    }

private:
    string m_name;
    std::shared_ptr<Person> m_partner;
};

int main()
{
    {
        auto shrp1 = std::make_shared<Person>("Lucy");
        auto shrp2 = std::make_shared<Person>("Ricky");
        partnerUp(shrp1, shrp2);  // 互相设为伙伴
    }
    return 0;
}

运行之后可以发现控制台只输出了两行created字符,也就是说对象并没有被析构,导致了内存的泄漏。当作用域结束时,理应执行析构函数,但是当析构shrp1时,却发现shrp2内部引用了shrp1,那么就得先析构shrp2,但是发现shrp1中内部又引用了shrp1,互相引用导致了 “ 死锁 ” ,最终导致内存泄漏

“自锁”同理。

auto shrp = std::make_shared<Person>("Lucy");
partnerUp(shrp, shrp);

那么这种情况下就需要使用到std::weak_ptr

std::weak_ptr

std::weak_ptr能够引用std::shared_ptr中的内存,但是它只作为旁观者的存在,并不享有内存的所有权,也就是说使用std::weak_ptr去接受一个std::shared_ptr,并不会增加强引用计数,当然也不会影响到std::shared_ptr的析构,有效的阻止了 “ 死锁 ”, “ 自锁 ” 的问题

class Person
{
    //Codes..
private:
    string m_name;
    std::weak_ptr<Person> m_partner;	//此时程序能够正常析构了
};

我们并不能直接访问std::weak_ptr中记录的数据,即std::weak_ptr没有operator*operator->之类的重载。std::weak_ptr更大的作用是作为一位旁观者

w_ptr的构建

std::weak_ptr一般通过std::shared_ptr来创建的,会增加源std::shared_ptr的弱引用计数,增加计数是原子操作,因此是线程安全的

std::shared_ptr<int> shrp = std::make_shared<int>(20);
std::weak_ptr<int> wkp(shrp);

若利用一个已经失效的std::weak_ptr去构建一个std::shared_ptr(注意std::shared_ptr接受std::weak_ptr的构造函数是显式的),那么会抛出std::bad_weak_ptr类型的异常

std::shared_ptr<int> shrp1 = std::make_shared<int>(20);
std::weak_ptr<int> wkp(shrp1);
shrp1 = nullptr;
// 抛出异常
std::shared_ptr<int> shrp2(wkp);

w_ptr的API

  • 使用lock()可以返回一个它所监视的std::shared_ptr。若std::weak_ptr失效,那么将会返回nullptr

  • 使用expired()可以用于检测std::weak_ptr是否失效

    bool expired() const noexcept {
        return this->use_count() == 0;
    }
    
  • 使用use_count()可以获取他所监视的std::shared_ptr的强引用计数

    std::shared_ptr<int> shrp = std::make_shared<int>(10);
    std::weak_ptr<int> wkp = shrp;
    auto shrp2 = wkp.lock();
    auto shrp3 = wkp.lock();
    std::cout << wkp.use_count() << std::endl;		// 3
    

    注意,std::weak_ptrstd::shared_ptr一样,都是派生自_Ptr_base的,use_count()是基类中的方法

w_ptr的应用场景

如Effective Modern C++中所说,若有一个运行代价高昂的函数,且传入的ID会频繁的重复

std::unique_ptr<const Widget> loadWidget(int ID);

那么为了提高效率,一个直观的方法就是使用哈希表对ID进行缓存

std::shared_ptr<const Widget> fastLoadWidget(int ID)
{
    static std::unordered_map<int, std::weak_ptr<const Widget>> cached;
    std::shared_ptr<const Widget> objPtr = cached[ID].lock();
    if (objPtr == nullptr)
    {
        // 使用std::unique_ptr构建std::shared_ptr
        objPtr = loadWidget(ID);
        // 使用std::shared_ptr构建std::weak_ptr
        cached[ID] = objPtr;
    }
    return objPtr;
}

树状环路问题

在树状结构中,可能存在父节点指向子节点,子节点指向父节点的环路。但是这种数据结构的特性是:子节点通常是被父节点拥有,子节点无法脱离父节点生存,当父节点被析构时,子节点也需要被析构

因此这种情况可以不使用std::shared_ptrstd::weak_ptr的组合,改为使用std::unique_ptr和原始指针,能在保证安全的前提下,效率更高

其他

std::make_unique的实现

运用变参模板,简单实现my_make_unique

// 不支持处理数组 
template<typename T, typename... Ts>
std::unique_ptr<T> my_make_unique(Ts&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Ts>(args)...));
}

这里在new T使对形参的完美转发采用的是小括号而非大括号,与标准库中的设计一致,因此:

// 创建一个含有10个元素的动态数组 元素的值都是20
std::unique_ptr<std::vector<int>> uniquePtr = my_make_unique<std::vector<int>>(10, 20);
// 数组中含有两个元素 分别是10和20
std::unique_ptr<std::vector<int>> uniquePtr = my_make_unique<std::vector<int>>(std::initializer_list<int>{10, 20});

完整的make_unique实现

// 普通构造函数
template <class _Ty, class... _Types, enable_if_t<!is_array_v<_Ty>, int> = 0>
unique_ptr<_Ty> make_unique(_Types&&... _Args) {
    return unique_ptr<_Ty>(new _Ty(_STD forward<_Types>(_Args)...));
}

// std::unique_ptr<T[]>
template <class _Ty, enable_if_t<is_array_v<_Ty> && extent_v<_Ty> == 0, int> = 0>
unique_ptr<_Ty> make_unique(const size_t _Size) {
    using _Elem = remove_extent_t<_Ty>;
    return unique_ptr<_Ty>(new _Elem[_Size]());
}

// 过滤定长数组
template <class _Ty, class... _Types, enable_if_t<extent_v<_Ty> != 0, int> = 0>
void make_unique(_Types&&...) = delete;

std::make_的优点

对于std::shared_ptr而言,内部维护了两个指针指向资源块和控制块,前者是智能指针管理的对象,后者用于记录引用次数。如果使用构造函数,利用一个指向堆上数组的指针初始化智能指针,那么控制块是单独分配的,即资源块与控制块处于两个内存中

内存

而若采用std::make_shared方法,资源块和控制块将分配到同块内存中,而在程序运行中,内存分配是代价高昂的操作,因此std::make_shared效率更高

另一个比较重要的点是异常安全,使用std::make_是异常安全的。考虑以下代码

void processWidget(std::shared_ptr<Widget> spw, int priority);
processWidget(std::shared_ptr<Widget>(new Widget()), computePriority());

当以上述的形式调用函数的时候,一共需要执行三件事

  • 一个Widget需要被new出来,即需要在堆上分配内存
  • 一个std::shared_ptr<Widget>对象需要被创建出来
  • computePriority()需要被调用

由于编译器可能会重排指令,导致computePriority()操作可能会发生中间,且此时computePriority()在执行途中抛出了异常,导致std::shared_ptr<Widget>没有被创建,进而导致new出来的对象没有办法被释放,导致内存泄漏

而因为std::make_是一个函数,所以在堆上分配内存和创建std::shared_ptr<Widget>是一个整体,不会被分割,所以是异常安全的

std::make_的缺陷

  • std::make_函数都不允许使用定制删除器,deleter只能在std::unique_ptrstd::shared_ptr的构造函数中传入

    // 先构建好一个智能指针对象再传入 使异常安全
    std::shared_ptr<Widget> spw(new Widget(), funcObj);
    // 移动传参 提高性能
    processWidget(std::move(spw), computePriority());
    
  • std::make_函数不能完美的传递std::initializer_list

    // auto unip = std::make_unique<std::vector<int>>({ 1,2,3 });	// 错误
    
    std::initializer_list<int> il = { 1,2,3 };
    auto unip = std::make_unique<std::vector<int>>(il);			// 正确
    
  • 当构造函数是私有或者保护时,无法std::make_std::make_实际上是类似调用到了构造函数。解决方法是将std::make_声明为友元函数

    class PrivateCtorClass
    {
        friend std::unique_ptr<PrivateCtorClass> std::make_unique<PrivateCtorClass>();
        PrivateCtorClass() : data(100) {}
    public:
        int data;
    };
    
    int main()
    {
        std::unique_ptr<PrivateCtorClass> up = std::make_unique<PrivateCtorClass>();
        std::cout << up->data << std::endl;
    }
    
  • 对于std::shared_ptr来说,对象的内存可能无法及时回收

    虽然std::make_shared减少了内存分配次数,提高了性能,但是在回收内存的时候存在一点问题:

    我们知道,当对象的引用计数降为0时(强引用次数降为0,与弱引用次数无关,std::weak_ptr只是个旁观者),对象被销毁(调用析构函数并释放内存)。但是对于资源快与控制块处于同一块内存上的情况而言,由于内存必须完整地申请释放,因此只有当控制块也可以被回收时,这块内存才会被释放。而控制块中包含了两个计数,一个为强引用(std::shared_ptr)计数,一个为弱引用(std::weak_ptr)计数,当两个引用计数都归为0时,控制块才能释放。那么也就是说只要还有一个std::weak_ptr指向控制块(弱引用计数 > 0),控制块就不能被释放,也就代表这块内存也不能被释放(即使std::shared_ptr已经离开作用域了)

    总的来说,通过std::make_shared分配出来的这块内存,只有当最后一个std::shared_ptrstd::weak_ptr都被销毁时,才会被释放。下面为验证代码

    int main()
    {
        // 64位的程序才能申请2G大小的内存
        std::shared_ptr<char[]> shrp(new char[INT32_MAX]);
        std::weak_ptr<char[]> wp(shrp);
        shrp.reset();
        // 在等待任意键的时候 shrp管理的资源已经得到释放 程序内存占用很低
        system("pause");
    }
    
    int main()
    {
        // 64位的程序才能申请2G大小的内存
        std::shared_ptr<char[]> shrp = std::make_shared<char[]>(INT32_MAX);
        std::weak_ptr<char[]> wp(shrp);
        shrp.reset();
        // 在等待任意键的时候 shrp管理的资源没有得到释放 程序内存占用高达2G
        system("pause");
    }
    

使用智能指针管理动态二维数组

正常我们申请动态二维数组是这么做的

int** p = new int*[3];
for (int i = 0; i < 3; i++)
    p[i] = new int[5]{};

for (int i = 0; i < 3; i++)
    delete[] p[i];
delete[] p;

有了智能指针后,我们可以做出这么一件明明可以用std::vector但就是偏偏要折磨自己的事情

// C++20下编译通过 创建一个3*5的元素都为10的二维数组
std::shared_ptr<std::shared_ptr<int[]>[]> sp = std::make_shared<std::shared_ptr<int[]>[]>(3);
for (int i = 0; i < 3; i++)
    sp[i] = std::make_shared<int[]>(5, 10);

智能指针的deleter

智能指针与动态数组

在C++17之前,std::shared_ptr的构造与std::unique_ptr存在一点差异:std::unique_ptr支持动态数组,而std::share_ptr不支持动态数组

std::unique_ptr<int[]> unip(new int[10]);	// 合法
std::shared_ptr<int[]> shrp(new int [10]);	// 在C++ 17之前时非法操作 不能传入数组类型作为模板参数
std::shared_ptr<int> shrp2(new int[10]);	// 可编译,但存在未定义行为

因此,对于shrp2而言,在它析构时会发生这种情况

int* p = new int[10];
delete p;

这对于new int[10] 而言肯定是非法的,对它应使用delete[]

自定义deleter-s_ptr

对此我们有两种解决方法,一种是传入std::default_delete,另外一种是自行构造删除器

std::shared_ptr<int> shrp(new int[10], std::default_delete<int[]>());
// lambda表达式转换为函数指针
std::shared_ptr<int> shrp1(new int[10], [](int* p) { delete[] p; });

void deleter(int* p) { delete[] p; }
std::shared_ptr<int> shrp2(new int[10], deleter);

// 还可以封装一个my_make_shared_dynamic_arr的方法
template<typename T>
decltype(auto) my_make_shared_dynamic_arr(std::size_t size)
{
    return std::shared_ptr<T>(new T[size], std::default_delete<T[]>());
}
std::shared_ptr<int> shrp3 = my_make_shared_dynamic_arr(10);

虽然说能用了,但存在着几个缺点

  • 我们想管理的是int[]类型,但std::shared_ptr的模板类型却为int
  • 需要显示提供删除器
  • 无法使用std::make_shared,无法保证异常安全
  • 由于std::shared_ptr<int>没有重载operator[],故需要使用.get()[Index]来获取数组中的元素

有人可能会说了,那使用std::shared_ptr<int*>不就行了吗,数组退化成指针

其实事情并没有这么简单

int* p = new int[10]{};
std::shared_ptr<int*> shrp(&p, [](int** p) { delete[] *p; });
std::cout << (*(shrp.get()))[5] << std::endl;

自定义deleter-u_ptr

对于std::shared_ptr来说,自定义一个删除器是比较简单的,而对于std::unique_ptr来说,情况有点不同

std::shared_ptr<int> shrp(new int(10), [](int* p) { delete p; });	// 正确
// std::unique_ptr<int> unip(new int(10), [](int* p) { delete p; });	// 错误

std::unique_ptr在指定删除器时需要在模板参数中传入删除器的类型

// 一个接受int*且无返回值的函数指针
std::unique_ptr<int,void(*)(int*)> unip(new int(10), [](int* p) { delete p; });

如果在lambda表达式中捕获了变量,那么需要使用std::function来进行包装,因为捕获了变量的lambda表达式无法转换为函数指针

// std::unique_ptr<int, void(*)(int*)> unip(new int(10), [&](int* p) {delete p; });
std::unique_ptr<int, std::function<void(int*)>> unip(new int(10), [&](int* p) { delete p; });

除了用lambda表达式,我们还可以这么干

template<typename T>
void deleter(T* p) { delete[] p; }

//使用decltype类型推断
std::unique_ptr<int, decltype(deleter<int>)*> unip(new int(10), deleter<int>);
//使用typedef取别名
typedef void(*deleter_int)(int*);
std::unique_ptr<int, deleter_int> unip(new int(10), deleter<int>);
//联合typedef与decltype
typedef decltype(deleter<int>)* deleter_int_TD;
std::unique_ptr<int, deleter_int_TD> unip(new int(10), deleter<int>);

利用using+decltype,间接直观

template<typename T>
void deleter(T* p) { delete[] p; }

template<typename T>
using deleter_t = decltype(deleter<T>)*;

int main() {
    std::unique_ptr<int, deleter_t<int>> unip(new int(10), deleter<int>);
}

C++17后的做法

在C++ 17中,std::shared_ptr支持传入T[]类型作为模板参数

std::shared_ptr<int[]> shrp(new int[10]{});
std::cout << shrp[3] << std::endl;	// 0

在C++20中,可以使用std::make_shared来创建智能指针

// 分配一个管理有10个int元素的动态数组的std::shared_ptr
std::shared_ptr<int[]> shrp = std::make_shared<int[]>(10);
// 同理也有
std::unique_ptr<int[]> unip = std::unique_ptr<int[]>(10);

手撕智能指针

struct second_ctor_tag {};
struct first_and_second_ctor_tag {};

// 只有空类和非final类才能压缩
template<typename FirstType, typename SecondType, bool = std::is_empty_v<FirstType> && !std::is_final_v<FirstType>>
class my_compressed_pair : private FirstType {
private:
    SecondType val;
public:
    template<typename... SecondParamTypes>
    my_compressed_pair(second_ctor_tag, SecondParamTypes&& ... params)
            : FirstType(), val(std::forward<SecondParamTypes>(params)...) {}

    template<typename FirstParamType, typename... SecondParamTypes>
    my_compressed_pair(first_and_second_ctor_tag, FirstParamType&& first_param, SecondParamTypes&& ... second_params)
            : FirstType(std::forward<FirstParamType>(first_param)),
              val(std::forward<SecondParamTypes>(second_params)...) {}

    inline const FirstType& get_first() const { return *this; }

    inline const SecondType& get_second() const { return val; }

    inline FirstType& get_first() { return *this; }

    inline SecondType& get_second() { return val; }
};

// 针对无法压缩的类的特化
template<typename FirstType, typename SecondType>
class my_compressed_pair<FirstType, SecondType, false> {
private:
    FirstType first_val;
    SecondType second_val;
public:
    template<typename... SecondParamTypes>
    my_compressed_pair(second_ctor_tag, SecondParamTypes&& ... params)
            : first_val(), second_val(std::forward<SecondParamTypes>(params)...) {}

    template<typename FirstParamType, typename... SecondParamTypes>
    my_compressed_pair(first_and_second_ctor_tag, FirstParamType&& first_param, SecondParamTypes&& ... second_params)
            : first_val(std::forward<FirstParamType>(first_param)),
              second_val(std::forward<SecondParamTypes>(second_params)...) {}


    inline const FirstType& get_first() const { return first_val; }

    inline const SecondType& get_second() const { return second_val; }

    inline FirstType& get_first() { return first_val; }

    inline SecondType& get_second() { return second_val; }
};

template<typename T>
struct default_deleter {
    void operator()(T*pointer) const {
        delete pointer;
    }
};

my_unique_ptr

  • 利用空类继承的Tricky,减少了仿函数以及无捕获Lambda表达式作为删除器时智能指针的空间占用
  • 显式的bool类型转换,否则在智能指针之间互相比较时会转换为对资源有效性检验结果的比较
  • 使用标签分发,调用不同的my_unique_ptr构造函数
  • 缺陷:包括但不限于以下几点,本实现仅供了解std::unique_ptr工作机制使用
    • 没有特化T[]版本的智能指针
    • 没有对各类模板类型做进一步的限制(如对返回引用类型施加add_lvalue_reference_t<T>
    • 没有实现多态类型智能指针的转换关系(如my_unique_ptr<son>my_unique_ptr<father>的转换)
template<typename T, typename U = default_deleter<T>>
class my_unique_ptr {
    using PointerType = std::remove_reference_t<T>*;
    using ElementType = T;
    using DeleterType = U;
private:
    my_compressed_pair<DeleterType, PointerType> compressed_pair;

public:
    template<typename = std::enable_if_t<std::is_class_v<DeleterType>, void>>	
    explicit my_unique_ptr(PointerType _data = nullptr) : compressed_pair(second_ctor_tag{}, _data) {}

    template<typename Dx, typename = std::enable_if_t<std::is_convertible_v<Dx, DeleterType>, void>>
    my_unique_ptr(PointerType _data, Dx&& _deleter)
            : compressed_pair(first_and_second_ctor_tag{}, std::forward<Dx>(_deleter), _data) {}

    my_unique_ptr(my_unique_ptr&& _another) : compressed_pair(first_and_second_ctor_tag{},
                                                              std::move(_another.get_deleter()), _another.release()) {}

    my_unique_ptr& operator=(my_unique_ptr&& _another) {
        if (this != &_another) {
            reset(_another.release());
            compressed_pair.get_first() = std::move(_another.get_deleter());
        }
        return *this;
    }

    my_unique_ptr(const my_unique_ptr&) = delete;

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

    ~my_unique_ptr() {
        reset();
    }

public:
    PointerType release() {
        PointerType originPointer = get();
        compressed_pair.get_second() = nullptr;
        return originPointer;
    }

    void reset(PointerType new_pointer = nullptr) {
        PointerType originPointer = get();
        compressed_pair.get_second() = new_pointer;
        compressed_pair.get_first()(originPointer);
    };

public:
    PointerType get() const {
        return compressed_pair.get_second();
    }

    PointerType operator->() const {
        return get();
    }

    ElementType& operator*() const {
        return *get();
    }

    const DeleterType& get_deleter() const {
        return compressed_pair.get_first();
    }

    DeleterType& get_deleter() {
        return compressed_pair.get_first();
    }

    void swap(my_unique_ptr& _another) {
        std::swap(compressed_pair.get_first(), _another.compressed_pair.get_first());
        std::swap(compressed_pair.get_second(), _another.compressed_pair.get_second());
    }

    explicit operator bool() const {
        return static_cast<bool>(compressed_pair.get_second());
    }
};

my_shared_ptr

  • 通过虚函数的调用使my_shared_ptr本身不需要包含删除器类型
  • 控制块中的计数是原子类型
  • release和移动赋值拷贝赋值本质上都是对swap进行巧妙操作
  • 缺陷:包括但不限于以下几点,本实现仅供了解std::shared_ptr工作机制使用
    • 没有特化T[]版本的智能指针
    • 没有对各类模板类型做进一步的限制(如对返回引用类型施加add_lvalue_reference_t<T>
    • 没有实现多态类型智能指针的转换关系(如my_shared_ptr<son>my_shared_ptr<father>的转换)
class base_control_block {
public:
    std::atomic<std::size_t> strong_count;
    std::atomic<std::size_t> weak_count;

    virtual void destroy() = 0;

    base_control_block() : strong_count(0), weak_count(0) {}
};

template<typename T, typename U = default_deleter<T>>
class control_block : public base_control_block {
public:
    using PointerType = std::remove_reference_t<T>*;
    using ElementType = T;
    using DeleterType = U;

    my_compressed_pair<DeleterType, PointerType> compressed_pair;

    template<typename = std::enable_if_t<std::is_class_v<DeleterType>, void>>
    control_block(PointerType _data = nullptr) : base_control_block(), compressed_pair(second_ctor_tag {}, _data) {}

    template<typename Dx, typename = std::enable_if_t<std::is_convertible_v<Dx, DeleterType>, void>>
    control_block(PointerType _data, Dx&& _deleter)
            : base_control_block(), compressed_pair(first_and_second_ctor_tag {}, std::forward<Dx>(_deleter), _data) {}

    void destroy() override {
        compressed_pair.get_first()(compressed_pair.get_second());
    }
};

template<typename T>
class my_shared_ptr {
public:
    using PointerType = std::remove_reference_t<T>*;
    using ElementType = T;
private:
    PointerType pResources;
    base_control_block*pControl;
public:
    my_shared_ptr(PointerType _newResources = nullptr)
            : pResources(_newResources), pControl(new control_block<T>(_newResources)) {
        ++pControl->strong_count;
    }

    template<typename DeleterType>
    my_shared_ptr(PointerType _newResources, DeleterType&& _deleter)
            : pResources(_newResources),
              pControl(new control_block<T, DeleterType>(_newResources, std::forward<DeleterType>(_deleter))) {
        ++pControl->strong_count;
    }

    my_shared_ptr(const my_shared_ptr& _copy) : pResources(_copy.pResources), pControl(_copy.pControl) {
        ++pControl->strong_count;
    }

    my_shared_ptr(my_shared_ptr&& _another) : pResources(_another.pResources), pControl(_another.pControl) {
        _another.pControl = nullptr;
        _another.pResources = nullptr;
    }

    template<typename _Ty, typename = std::enable_if_t<std::is_same_v<_Ty, my_shared_ptr<ElementType>>, void>>
    void operator=(_Ty&& _uni) {
        my_shared_ptr(std::forward<_Ty>(_uni)).swap(*this);
    }

    ~my_shared_ptr() {
        if (pControl != nullptr) {
            --pControl->strong_count;
            if (pControl->strong_count == 0)
                pControl->destroy();
        }
    }

public:
    PointerType get() const {
        return pResources;
    }

    PointerType operator->() const {
        return get();
    }

    const ElementType& operator*() const {
        return *pResources;
    }

    ElementType& operator*() {
        return *pResources;
    }

    void release() {
        my_shared_ptr{}.swap(*this);
    }

    void swap(my_shared_ptr& _another) {
        std::swap(this->pControl, _another.pControl);
        std::swap(this->pResources, _another.pResources);
    }

    std::size_t use_count() const {
        return pControl->strong_count;
    }

    explicit operator bool() const {
        return static_cast<bool>(pResources);
    }
};
posted @ 2020-10-22 16:05  _FeiFei  阅读(1093)  评论(5编辑  收藏  举报