В отличие от 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.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <iostream> #include <memory> // для std::shared_ptr class Item { public: Item() { std::cout << "Item acquired\n"; } ~Item() { std::cout << "Item destroyed\n"; } }; int main() { // Выделяем Item и передаем его в std::shared_ptr Item *item = new Item; std::shared_ptr<Item> ptr1(item); { std::shared_ptr<Item> ptr2(ptr1); // используем копирующую инициализацию для создания второго std::shared_ptr из ptr1, указывающего на тот же Item std::cout << "Killing one shared pointer\n"; } // ptr2 выходит из области видимости здесь, но больше ничего не происходит std::cout << "Killing another shared pointer\n"; return 0; } // ptr1 выходит из области видимости здесь, и выделенный Item уничтожается также здесь |
Результат выполнения программы:
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.
Обратите внимание, мы создали второй умный указатель из первого умного указателя (используя оператор присваивания копированием). Это важно.
Рассмотрим следующую программу:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <iostream> #include <memory> // для std::shared_ptr class Item { public: Item() { std::cout << "Item acquired\n"; } ~Item() { std::cout << "Item destroyed\n"; } }; int main() { Item *item = new Item; std::shared_ptr<Item> ptr1(item); { std::shared_ptr<Item> ptr2(item); // создаем ptr2 напрямую из item (вместо ptr1) std::cout << "Killing one shared pointer\n"; } // ptr2 выходит из области видимости здесь, и выделенный Item уничтожается также здесь std::cout << "Killing another shared pointer\n"; return 0; } // ptr1 выходит из области видимости здесь, и уже удаленный 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():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <iostream> #include <memory> // для std::shared_ptr class Item { public: Item() { std::cout << "Item acquired\n"; } ~Item() { std::cout << "Item destroyed\n"; } }; int main() { // Выделяем Item и передаем его в std::shared_ptr auto ptr1 = std::make_shared<Item>(); { auto ptr2 = ptr1; // создаем ptr2 из ptr1, используя семантику копирования std::cout << "Killing one shared pointer\n"; } // ptr2 выходит из области видимости здесь, но ничего больше не происходит std::cout << "Killing another shared pointer\n"; return 0; } // ptr1 выходит из области видимости здесь, и выделенный Item также уничтожается здесь |
Причины использования функции 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, указывающие на него.
Можете пожалуйста рассказать о std::enable_shared_from_this<T>. О том, как его использовать, зачем он нужен. Почему вместо shared_from_this() нельзя просто использовать std::make_shared<T>(*this)?
Можно ли создать несколько умных указателей путем последовательной семантики копирования: ptr1(item); ptr2(ptr1); ptr3(ptr2), а потом удалить ptr2? Если да, то будут ли ptr1 и ptr3 знать о друг друга?
Вообще по логике должны знать, ибо при создании ptr3 количество указателей которых указывают на один объект увеличивается и соответственно равно 3 , потом удаляется ptr2 и объектов становится 2.
И вот небольшая програмка что бы это проверить:
Надеюсь кому то помог. Сам сейчас изучаю c++ поэтому если я написал что то не правильно — исправьте
Можно просто auto dosm( const Item* ptr)