M.7 – std::shared_ptr

Добавлено 21 сентября 2021 в 23:27

В отличие от 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.

Теги

C++ / CppLearnCppstd::make_sharedstd::shared_ptrSTL / Standard Template Library / Стандартная библиотека шаблоновДля начинающихОбучениеПрограммированиеУмные указатели

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.