Урок №194. Умный указатель std::shared_ptr

  Юрий  | 

  |

  Обновл. 8 Окт 2021  | 

 58629

 ǀ   5 

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

Результат выполнения программы:

Item acquired
Killing one shared pointer
Killing another shared pointer
Item destroyed

Здесь мы динамически выделяем объект класса Item и передаем его указателю std::shared_ptr с именем ptr1. Внутри вложенного блока функции main() мы используем семантику копирования (которая разрешена в std::shared_ptr, так как одним ресурсом могут владеть сразу несколько умных указателей) для создания второго std::shared_ptr (ptr2), который указывает на тот же выделенный Item. Когда ptr2 выходит из области видимости, Item не уничтожается, так как ptr1 по-прежнему указывает на него. Когда ptr1 выходит из области видимости, то он замечает, что больше нет std::shared_ptr, которые бы указывали на Item, и удаляет Item.

Обратите внимание, мы создали второй умный указатель из первого умного указателя (используя оператор присваивания копированием). Это важно.

Рассмотрим следующую программу:

Результат выполнения программы:

Item acquired
Killing one shared pointer
Item destroyed
Killing another shared pointer
Item destroyed

И затем «Бум!» — генерация исключения (по крайней мере, на компьютере автора).

Разница здесь в том, что мы создали два отдельных, независимых друг от друга умных указателя std::shared_ptr. Хотя они оба указывают на один и тот же Item, они не знают о существовании друг друга. Когда ptr2 выходит из области видимости, он думает, что является единственным владельцем Item-а, поэтому уничтожает его. Когда позже ptr1 выходит из области видимости, он думает так же и пытается снова удалить (уже удаленный) Item. Бум!

К счастью, этого легко избежать, используя семантику копирования для создания нескольких умных указателей, указывающих на один динамически выделенный ресурс.

Правило: Всегда выполняйте копирование существующего std::shared_ptr, если вам нужно более одного std::shared_ptr, указывающего на один и тот же динамически выделенный ресурс.

Функция std::make_shared()


Как функцию std::make_unique() можно использовать для создания std::unique_ptr в C++14, так и функцию std::make_shared() можно (и нужно) использовать для создания std::shared_ptr. Функцию std::make_shared() добавили в C++11.

Давайте перепишем первую программу из данного урока, добавив функцию std::make_shared():

Причины использования функции std::make_shared() такие же, как и причины использования функции std::make_unique(): проще, безопаснее и производительнее за счет того, что 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::shared_ptr из std::unique_ptr


Умный указатель std::unique_ptr может быть конвертирован в умный указатель std::shared_ptr через специальный конструктор std::shared_ptr, который принимает std::unique_ptr в качестве r-value. Таким образом, содержимое 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::shared_ptr, тоже не будет удален. С std::unique_ptr вам нужно беспокоиться об удалении лишь одного указателя. А с std::shared_ptr вам придется беспокоиться об удалении всех указателей, владеющих ресурсом. Если какой-либо из std::shared_ptr, владеющий ресурсом, не будет должным образом уничтожен, то и сам ресурс также не будет уничтожен.

Умный указатель std::shared_ptr и массивы


В C++14 и в более ранних версиях С++ std::shared_ptr не имеет поддержки управления динамическими массивами и, соответственно, не должен использоваться с ними. Начиная с C++17, в std::shared_ptr добавили поддержку динамических массивов. Однако в C++17 «забыли» добавить эту поддержку в std::make_shared(), поэтому данную функцию не следует использовать для создания std::shared_ptr, указывающего на динамические массивы.

Заключение

Умный указатель std::shared_ptr дает возможность сразу нескольким умным указателям управлять одним динамически выделенным ресурсом. Ресурс уничтожается лишь в том случае, когда уничтожены все std::shared_ptr, указывающие на него.

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (169 оценок, среднее: 4,88 из 5)
Загрузка...

Комментариев: 5

  1. Олександр:

    В комментариях под прошлыми уроками видел ребят которые «изобрели велосипед», а другие им говорили что уже существует в стандартной библиотеке std::shared_ptr. Я предполагал что запоминание количества указателей владеющих объектом будет реализовано через статическую переменную класса. В этом уроке я увидел реализацию через динамически выделенный объект. При такой реализации возникает проблема создания умного указателя не через копирующую инициализацию, чего с статической переменной можно было бы избежать. Так зачем использовать динамический объект, а не статическую переменную?

  2. Irina:

    Можете пожалуйста рассказать о std::enable_shared_from_this<T>. О том, как его использовать, зачем он нужен. Почему вместо shared_from_this() нельзя просто использовать std::make_shared<T>(*this)?

  3. IRONSAID:

    Можно ли создать несколько умных указателей путем последовательной семантики копирования: ptr1(item); ptr2(ptr1); ptr3(ptr2), а потом удалить ptr2? Если да, то будут ли ptr1 и ptr3 знать о друг друга?

    1. vlad:

      Вообще по логике должны знать, ибо при создании ptr3 количество указателей которых указывают на один объект увеличивается и соответственно равно 3 , потом удаляется ptr2 и объектов становится 2.
      И вот небольшая програмка что бы это проверить:

      Надеюсь кому то помог. Сам сейчас изучаю c++ поэтому если я написал что то не правильно — исправьте

      1. vlad:

        Можно просто auto dosm( const Item* ptr)

Добавить комментарий

Ваш E-mail не будет опубликован. Обязательные поля помечены *