Урок №202. Розумний вказівник std::shared_ptr

  Юрій  | 

  Оновл. 31 Бер 2021  | 

 80

На відміну від 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, який вказує на динамічні масиви. Можливо, ця проблема буде вирішена в C++20.

Висновки

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

Оцінити статтю:

1 Зірка2 Зірки3 Зірки4 Зірки5 Зірок (1 оцінок, середня: 5,00 з 5)
Loading...

Залишити відповідь

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