На відміну від 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, що вказують на нього.