Item 19: Use std::shared_ptr
for shared-ownership resource management
std::shared_ptr 是一个支持共享所有权对象资源管理的智能指针。它通过引用计数来确保是否是最后一个指向某种资源的指针,如果引用计数值为零,即不再指向对象时,最后一个 std::shared_ptr就会在析构函数中销毁资源。
引用计数有几个性能影响:
std::shared_ptr
大小是原始指针的两倍,因为它内部包含一个指向资源的原始指针,还包含一个指向资源的引用计数值的原始指针。- 引用计数的内存必须动态分配。
- 递增递减引用计数必须是原子性的。
std::shared_ptr
和std::unique_ptr
一样也支持自定义的删除器。但是,删除器类型不是std::shared_ptr
的一部分。
1 | auto loggingDel = [](Widget *pw) //自定义删除器 |
考虑有两个std::shared_ptr<Widget>
,虽然带有不同的删除器,但他们是同一类型。
1 | auto customDeleter1 = [](Widget *pw) { … }; //自定义删除器, |
此外,自定义删除器不会改变std::shared_ptr
对象的大小。std::shared_ptr
内部包含一个指向资源的原始指针,还包含一个指向控制块的指针。一个控制块包含引用计数、弱计数和指向自定义删除器、分配器等的额外功能指针。每个std::shared_ptr
管理的对象都有个相应的控制块。
控制块的创建会遵循下面几条规则:
std::make_shared
总是创建一个控制块。- 当从独占指针(即
std::unique_ptr
或者std::auto_ptr
)上构造出std::shared_ptr
时会创建控制块。 - 当从原始指针上构造出
std::shared_ptr
时会创建控制块。
关于第3点, 一般不建议从原始指针上构造一个std::shared_ptr
,因为指向的对象容易有多个控制块关联,从而对象将会被销毁多次。如以下这个例子:
1 | auto pw = new Widget; //pw是原始指针 |
因此,避免传给std::shared_ptr
构造函数原始指针,而是使用std::make_shared
。如果使用了自定义删除器导致无法使用std::make_shared
,直接传new
出来的结果,而不要传指针变量。
1 | std::shared_ptr<Widget> spw1(new Widget, //直接使用new的结果 |
一个比较意外的地方是使用this
指针作为std::shared_ptr
构造函数实参可能创建多个控制块。
1 | std::vector<std::shared_ptr<Widget>> processedWidgets; |
向std::shared_ptr
的容器传递一个原始指针(this
),根据控制块的创建规则3,std::shared_ptr
会为指向的Widget
(*this
)创建一个控制块。但是成员函数外面早已存在指向那个Widget
对象的指针。
std::shared_ptr
API有处理这种情况的模板类:std::enable_shared_from_this<T>
和成员函数shared_from_this()
。
1 | class Widget: public std::enable_shared_from_this<Widget> { |
std::unique_ptr
可以转换为std::shared_ptr
,但是反之不行。
std::shared_ptr
不支持数组的资源管理。到C++20才支持。
重点
std::shared_ptr 为有共享所有权的任意资源提供一种自动垃圾回收的便捷方式。
与 std::unique_ptr 相比, std::shared_ptr 对象通常大两倍, 会产生控制块的开销, 需要原子性的引用计数修改操作。
默认资源销毁是通过delete, 但支持自定义删除器. 删除器的类型对 std::shared_ptr 的声明类型没有影响。
避免从原始指针变量创建 std::shared_ptr。