C++

Effective Modern C++(5): 智能指针

Posted by keys961 on June 6, 2022

1. std::unique_ptr:独占资源

类似Rust的默认行为,独占一个资源,资源只有一个所属者。

  • 它出了作用域后,会自动释放指向的资源

    • 通过内部调用delete

    • 可自行设置删除回调:模版第二个参数,为一个函数,传入T指针为参数

  • 它一般为指针的大小,非常快,但只支持move

    • 若删除器是有状态的,则会增加std::unique_ptr的大小
  • 容易将std::unique_ptr转换成std::shared_ptr:通过move即可

2. std::shared_ptr:共享资源

类似Rust的Rc<T>,共享一个资源,通过引用计数管理资源。

  • 引用计数控制资源

  • 对比std::unique_ptr,大小通常大2倍,因为包含一个指针和一个控制块(动态分配的)指针

    item19_fig1

    • 上图构造可见其性能不如std::unique_ptr

    • 由于上图的构造,避免传入相同指针到不同的std::shared_ptr中,避免重复析构

    • 引用计数修改通过原子操作,所以有额外开销

  • 支持拷贝赋值,效果是引用计数+1

  • 避免循环引用,打破的一种方式是std::weak_ptr

3. std::weak_ptrstd::shared_ptr悬空时使用

std::weak_ptrstd::shared_ptr上创建,weak_cnt会增加1,但引用计数不增加,很像Java的虚引用。

其指向的资源可能会被std::shared_ptr释放(即悬空),此时需要检查,原子性检查可通过lock()函数,它:

  • 若没释放,返回一个std::shared_ptr,并且指向的引用计数+1

  • 若释放,则返回空指针

std::weak_ptr的结构和std::shared_ptr类似,前者指向后者相同的控制块,对于weak_cnt的操作也是原子的。

潜在使用场景:缓存、观察者列表、打破std::shared_ptr循环引用

4. 优先使用std::make_unique/std::make_shared

使用make_xxx的好处:

  • 代码简短,提高异常安全性

  • 数据和控制块是连续的,在一块内存里,更紧凑,一般而言更小更快

但也有限制:

  • 不支持自定义删除回调

  • 花括号初始化受限(C++20部分编译器已经支持)

  • 自定义newdelete的内存管理,不适合交给make_shared管理

    • 因为make_shared将控制块和数据放在一块,可能和自定义内存管理冲突
  • 大对象不适合make_shared,原因同上

  • std::weak_ptr比对应std::shared_ptr活的更久,也不适合make_shared

    • 原因同上:只有当std::weak_ptr死光后才能删除控制块,而控制块和数据放在一起,数据最后才能释放,即使引用计数为0

5. 当使用Pimpl惯用法,在实现文件中定义特殊成员函数

这里只讨论std::unique_ptr的情况。

一个样例就是:

1
2
3
4
5
6
7
8
// In header
class T {
public: 
// ...
private:
  struct Data;
  std::unique_ptr<Data> ptr; 
}
1
2
3
4
5
6
// In cpp
#include "t.h"
struct T::Data {
  // ...
}
// ...

即用一个指针指向一个数据struct,这样就可以:

  • 方便访问数据成员

  • 依赖更少的头文件,减少编译时间

这里Data是未完成类型。

而这里,特殊的成员函数,如移动、析构、构造函数等,需要先在头文件声明,然后再到实现文件中定义。(否则很可能过不了编译检查,因为检查前Data是未完成类型)