Effective Modern C++ Item 19

Item 19: Use std::shared_ptr for shared-ownership resource management

std::shared_ptr 是一个支持共享所有权对象资源管理的智能指针。它通过引用计数来确保是否是最后一个指向某种资源的指针,如果引用计数值为零,即不再指向对象时,最后一个 std::shared_ptr就会在析构函数中销毁资源。

引用计数有几个性能影响:

  • std::shared_ptr大小是原始指针的两倍,因为它内部包含一个指向资源的原始指针,还包含一个指向资源的引用计数值的原始指针。
  • 引用计数的内存必须动态分配
  • 递增递减引用计数必须是原子性的

std::shared_ptrstd::unique_ptr一样也支持自定义的删除器。但是,删除器类型不是std::shared_ptr的一部分。

1
2
3
4
5
6
7
8
9
10
11
auto loggingDel = [](Widget *pw)        //自定义删除器
{ //(和条款18一样)
makeLogEntry(pw);
delete pw;
};

std::unique_ptr< //删除器类型是
Widget, decltype(loggingDel) //指针类型的一部分
> upw(new Widget, loggingDel);
std::shared_ptr<Widget> //删除器类型不是
spw(new Widget, loggingDel); //指针类型的一部分

考虑有两个std::shared_ptr<Widget>,虽然带有不同的删除器,但他们是同一类型。

1
2
3
4
5
auto customDeleter1 = [](Widget *pw) { … };     //自定义删除器,
auto customDeleter2 = [](Widget *pw) { … }; //每种类型不同
std::shared_ptr<Widget> pw1(new Widget, customDeleter1);
std::shared_ptr<Widget> pw2(new Widget, customDeleter2);
std::vector<std::shared_ptr<Widget>> vpw{ pw1, pw2 };

此外,自定义删除器不会改变std::shared_ptr对象的大小。std::shared_ptr内部包含一个指向资源的原始指针,还包含一个指向控制块的指针。一个控制块包含引用计数、弱计数和指向自定义删除器、分配器等的额外功能指针。每个std::shared_ptr管理的对象都有个相应的控制块。

image-20240321000449021

控制块的创建会遵循下面几条规则:

  • std::make_shared总是创建一个控制块。
  • 当从独占指针(即std::unique_ptr或者std::auto_ptr)上构造出std::shared_ptr时会创建控制块。
  • 当从原始指针上构造出std::shared_ptr时会创建控制块。

关于第3点, 一般不建议从原始指针上构造一个std::shared_ptr,因为指向的对象容易有多个控制块关联,从而对象将会被销毁多次。如以下这个例子:

1
2
3
4
5
auto pw = new Widget;                           //pw是原始指针

std::shared_ptr<Widget> spw1(pw, loggingDel); //为*pw创建控制块

std::shared_ptr<Widget> spw2(pw, loggingDel); //为*pw创建第二个控制块

因此,避免传给std::shared_ptr构造函数原始指针,而是使用std::make_shared。如果使用了自定义删除器导致无法使用std::make_shared,直接传new出来的结果,而不要传指针变量。

1
2
3
std::shared_ptr<Widget> spw1(new Widget,    //直接使用new的结果
loggingDel);
std::shared_ptr<Widget> spw2(spw1); //spw2使用spw1一样的控制块

一个比较意外的地方是使用this指针作为std::shared_ptr构造函数实参可能创建多个控制块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
std::vector<std::shared_ptr<Widget>> processedWidgets;

class Widget {
public:

void process();

};

void Widget::process()
{
//处理Widget
processedWidgets.emplace_back(this); //然后将它加到已处理过的Widget
}

std::shared_ptr的容器传递一个原始指针(this),根据控制块的创建规则3,std::shared_ptr会为指向的Widget*this)创建一个控制块。但是成员函数外面早已存在指向那个Widget对象的指针。

std::shared_ptrAPI有处理这种情况的模板类:std::enable_shared_from_this<T>和成员函数shared_from_this()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Widget: public std::enable_shared_from_this<Widget> {
public:

void process();

};

void Widget::process()
{
//和之前一样,处理Widget

//把指向当前对象的std::shared_ptr加入processedWidgets
processedWidgets.emplace_back(shared_from_this());
}

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