M.7 – std::shared_ptr
В отличие от std::unique_ptr
, который предназначен для единоличного владения и управления ресурсом, std::shared_ptr
предназначен для решения ситуации, когда вам нужно несколько умных указателей, совместно владеющих ресурсом.
Это означает, что можно иметь несколько std::shared_ptr
, указывающих на один и тот же ресурс. Внутри std::shared_ptr
отслеживает, сколько std::shared_ptr
совместно используют этот ресурс. Пока хотя бы один std::shared_ptr
указывает на ресурс, этот ресурс не будет освобожден, даже если отдельные std::shared_ptr
будут уничтожены. Как только последний std::shared_ptr
, управляющий ресурсом, выйдет из области видимости (или будет переприсвоен, чтобы указывать на что-то еще), ресурс будет освобожден.
Как и std::unique_ptr
, std::shared_ptr
находится в заголовке <memory>
.
#include <iostream>
#include <memory> // для std::shared_ptr
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
int main()
{
// Размещаем объект Resource и
// оставляем его во владении std::shared_ptr
Resource *res = new Resource;
std::shared_ptr<Resource> ptr1(res);
{
// создаем еще один std::shared_ptr, указывающий на то же самое
std::shared_ptr<Resource> ptr2 {ptr1};
std::cout << "Killing one shared pointer\n";
} // ptr2 здесь выходит из области видимости, но ничего не происходит
std::cout << "Killing another shared pointer\n";
return 0;
} // ptr1 здесь выходит из области видимости,
// и выделенный Resource уничтожается
Этот код печатает:
Resource acquired
Killing one shared pointer
Killing another shared pointer
Resource destroyed
В приведенном выше коде мы создаем динамический объект Resource
и устанавливаем std::shared_ptr
с именем ptr1
для управления им. Внутри вложенного блока мы используем конструктор копирования для создания второго std::shared_ptr
(ptr2
), указывающего на тот же ресурс. Когда ptr2
выходит за пределы области видимости, ресурс не освобождается, потому что на него всё еще указывает ptr1
. Когда ptr1
выходит за пределы области видимости, ptr1
замечает, что больше нет указателей std::shared_ptr
, управляющих объектом Resource
, поэтому он освобождает этот объект Resource
.
Обратите внимание, что мы создали второй указатель совместного использования из первого указателя совместного использования. Это важно. Рассмотрим следующую аналогичную программу:
#include <iostream>
#include <memory> // для std::shared_ptr
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
int main()
{
Resource *res = new Resource;
std::shared_ptr<Resource> ptr1(res);
{
// создаем ptr2 прямо из res (вместо ptr1)
std::shared_ptr<Resource> ptr2(res);
std::cout << "Killing one shared pointer\n";
} // ptr2 здесь выходит из области видимости,
// и выделенный объект Resource уничтожается
std::cout << "Killing another shared pointer\n";
return 0;
} // ptr1 здесь выходит из области видимости,
// а выделенный объект Resource уничтожается снова
Эта программа печатает:
Resource acquired
Killing one shared pointer
Resource destroyed
Killing another shared pointer
Resource destroyed
а потом вылетает (по крайней мере, на машине автора).
Разница в том, что мы создали два std::shared_ptr
независимо друг от друга. Как следствие, даже если они оба указывают на один и тот же объект Resource
, они не знают друг о друге. Когда ptr2
выходит за пределы области видимости, он считает, что он единственный владелец объекта Resource
, и освобождает его. Когда ptr1
позже тоже выходит за пределы области видимости, он думает то же самое и снова пытается удалить объект Resource
. Потом случается плохое.
К счастью, этого легко избежать: если вам нужно более одного std::shared_ptr
для заданного ресурса, скопируйте существующий std::shared_ptr
.
Лучшая практика
Если вам нужно более одного std::shared_ptr
, указывающего на один и тот же ресурс, всегда делайте копию существующего std::shared_ptr
.
std::make_shared
Подобно тому, как в C++14 std::make_unique()
может использоваться для создания std::unique_ptr
, std::make_shared()
может (и должен) использоваться для создания std::shared_ptr
. std::make_shared()
доступен, начиная с C++11.
Вот наш исходный пример, переписанный на использование std::make_shared()
:
#include <iostream>
#include <memory> // для std::shared_ptr
class Resource
{
public:
Resource() { std::cout << "Resource acquired\n"; }
~Resource() { std::cout << "Resource destroyed\n"; }
};
int main()
{
// Размещаем объект Resource и
// оставляем его во владении std::shared_ptr
auto ptr1 = std::make_shared<Resource>();
{
auto ptr2 = ptr1; // // создаем ptr2, используя инициализацию копирования ptr1
std::cout << "Killing one shared pointer\n";
} // ptr2 здесь выходит из области видимости, но ничего не происходит
std::cout << "Killing another shared pointer\n";
return 0;
} // ptr1 здесь выходит из области видимости,
// и выделенный объект Resource уничтожается
Причины использования std::make_shared()
те же, что и std::make_unique()
– std::make_shared()
проще и безопаснее (используя этот способ, нет возможности напрямую создать два std::shared_ptr
, указывающих на один и тот же ресурс). Однако std::make_shared()
также более производительна, чем ее неиспользование. Причины этого кроются в том, как std::shared_ptr
отслеживает количество указателей, указывающих на заданный ресурс.
Копаемся в std::shared_ptr
В отличие от std::unique_ptr
, который внутри использует один указатель, std::shared_ptr
внутри использует два указателя. Один указатель указывает на управляемый ресурс. Другой указывает на «блок управления», который представляет собой динамически размещаемый объект, который отслеживает множество вещей, включая количество std::shared_ptr
, указывающих на ресурс. Когда std::shared_ptr
создается с помощью конструктора std::shared_ptr
, память для управляемого объекта (который обычно передается в конструктор) и управляющего блока (который создает конструктор) выделяется отдельно. Однако при использовании std::make_shared()
это можно оптимизировать до одного выделения памяти, что приведет к повышению производительности.
Это также объясняет, почему независимое создание двух std::shared_ptr
, указывающих на один и тот же ресурс, вызывает проблемы. Каждый std::shared_ptr
будет иметь один указатель, указывающий на ресурс. Однако каждый std::shared_ptr
будет независимо размещать свой собственный блок управления, который будет указывать на то, что это единственный указатель, владеющий этим ресурсом. Таким образом, когда этот std::shared_ptr
выходит за пределы области видимости, он освобождает ресурс, не понимая, что есть другие std::shared_ptr
, также пытающиеся управлять этим ресурсом.
Однако, когда std::shared_ptr
клонируется с помощью присваивания копированием, данные в блоке управления могут быть соответствующим образом обновлены, чтобы указать, что теперь есть дополнительные std::shared_ptr
, совместно управляющие заданным ресурсом.
Указатели совместного использования могут быть созданы из уникальных указателей
std::unique_ptr
может быть преобразован в std::shared_ptr
с помощью специального конструктора std::shared_ptr
, который принимает r-значение std::unique_ptr
. Содержимое std::unique_ptr
будет перемещено в std::shared_ptr
.
Однако std::shared_ptr
нельзя безопасно преобразовать в std::unique_ptr
. Это означает, что если вы создаете функцию, которая будет возвращать умный указатель, вам лучше вернуть std::unique_ptr
и присвоить его std::shared_ptr
, если и когда это будет уместно.
Опасности std::shared_ptr
std::shared_ptr
имеет некоторые из тех же проблем, что и std::unique_ptr
– если std::shared_ptr
не удаляется должным образом (либо потому, что он был динамически размещен и никогда не удаляется, либо он был частью объекта, который был динамически размещен и никогда не удаляется), то ресурс, которым он управляет, также не будет освобожден. С std::unique_ptr
вам нужно позаботиться только о том, чтобы один умный указатель был удален правильно. С std::shared_ptr
вам нужно беспокоиться обо всех из них. Если какой-либо из std::shared_ptr
, управляющий ресурсом, не будет должным образом уничтожен, то и ресурс не будет освобожден.
std::shared_ptr
и массивы
В C++17 и ранее std::shared_ptr
не имеет необходимой поддержки для управления массивами и не должен использоваться для управления массивами в стиле C. Начиная с C++20, std::shared_ptr
поддерживает массивы.
Заключение
std::shared_ptr
разработан для ситуаций, когда вам нужно несколько умных указателей, совместно управляющих одним и тем же ресурсом. Этот ресурс будет освобожден, когда будет уничтожен последний управляющий им std::shared_ptr
.