На попередньому уроці ми розглянули розумний вказівник std::shared_ptr і те, як з його допомогою відразу кілька розумних вказівників можуть володіти одним динамічно виділеним ресурсом. Однак, іноді це може бути проблематично.
Перетин розумних вказівників
Наприклад, розглянемо випадок, коли два розумних вказівники типу std::shared_ptr володіють двома різними об’єктами і «перетинаються» між собою:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
#include <iostream> #include <memory> // для std::shared_ptr #include <string> class Human { std::string m_name; std::shared_ptr<Human> m_partner; // спочатку порожній public: Human(const std::string &name): m_name(name) { std::cout << m_name << " created\n"; } ~Human() { std::cout << m_name << " destroyed\n"; } friend bool partnerUp(std::shared_ptr<Human> &h1, std::shared_ptr<Human> &h2) { if (!h1 || !h2) return false; h1->m_partner = h2; h2->m_partner = h1; std::cout << h1->m_name << " is now partnered with " << h2->m_name << "\n"; return true; } }; int main() { auto anton = std::make_shared<Human>("Anton"); // створення розумного вказівника з об'єктом Anton класу Human auto ivan = std::make_shared<Human>("Ivan"); // створення розумного вказівника з об'єктом Ivan класу Human partnerUp(anton, ivan); // Anton вказує на Ivan-а, а Ivan вказує на Anton-а return 0; } |
Тут ми динамічно виділяємо два об’єкти (Anton
і Ivan
) класу Human і, використовуючи std::make_shared, передаємо їх в два окремо створених розумних вказівники типу std::shared_ptr. Потім «пов’язуємо» їх за допомогою дружньої функції partnerUp().
Результат виконання програми:
Anton created
Ivan created
Anton is now partnered with Ivan
І це все? Ніяких знищень? Чому? Зараз розберемося.
Після виклику функції partnerUp() у нас є 4 розумних вказівники типу std::shared_ptr:
Два розумних вказівники вказують на об’єкт Ivan
: ivan
(з функції main()) і m_partner
(з класу Human) об’єкта Anton
.
Два розумних вказівники вказують на об’єкт Anton
: anton
і m_partner
об’єкта Ivan
.
В кінці функції partnerUp() розумний вказівник ivan
виходить з області видимості першим. Коли це відбувається, він перевіряє, чи є інші розумні вказівники, які володіють об’єктом Ivan
класу Human. Є — m_partner
об’єкта Anton
. Через це розумний вказівник не знищує Ivan
-а (якщо він це зробить, то m_partner
об’єкта Anton
залишиться висячим вказівником). Таким чином у нас залишається один розумний вказівник, який володіє Ivan
-ом (m_partner
об’єкта Anton
) і два розумних вказівники, які володіють Anton
-ом (anton
і m_partner
об’єкта Ivan
).
Потім розумний вказівник anton
виходить з області видимості, і відбувається те ж саме. anton
перевіряє, чи є інші розумні вказівники, які також володіють об’єктом Anton
класу Human. Є — m_partner
об’єкта Ivan
, тому об’єкт Anton
не знищується. Таким чином, залишаються два розумних вказівники:
m_partner
об’єкта Ivan
, який вказує на Anton
;
m_partner
об’єкта Anton
, який вказує на Ivan
.
Потім програма завершує своє виконання, і ні об’єкт Anton
, ні об’єкт Ivan
не знищуються! По суті, Anton
не дає знищити Ivan
-а, а Ivan
не дає знищити Anton
-а.
Це той випадок, коли розумні вказівники типу std::shared_ptr формують циклічну залежність.
Циклічна залежність
Циклічна залежність (або «циклічні посилання») — це серія «посилань», де поточний об’єкт посилається на наступний, а останній об’єкт посилається на перший. Ці «посилання» не обов’язково повинні бути звичайними посиланнями в мові C++, вони можуть бути вказівниками, унікальними ID або будь-якими іншими засобами ідентифікації конкретних об’єктів.
В контексті std::shared_ptr цими «посиланнями» є вказівники.
Це саме те, що ми бачимо вище: Anton
вказує на Ivan
-а, а Ivan
вказує на Anton
-а. Аналогічно, A
вказує на B
, B
вказує на C
, а C
вказує на A
. Практична цінність такої циклічної залежності в тому, що поточний об’єкт «залишає в живих» (не дає знищити) наступний об’єкт.
Спрощена циклічна залежність
Виявляється, проблема циклічного посилання може виникнути навіть з одним розумним вказівником типу std::shared_ptr. Така циклічна залежність називається спрощеною. Хоча це рідко трапляється на практиці, але все ж розглянемо і цей випадок:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <iostream> #include <memory> // для std::shared_ptr class Item { public: std::shared_ptr<Item> m_ptr; // спочатку порожній Item() { std::cout << "Item acquired\n"; } ~Item() { std::cout << "Item destroyed\n"; } }; int main() { auto ptr1 = std::make_shared<Item>(); ptr1->m_ptr = ptr1; // m_ptr тепер є власником Item-а, членом якого він є сам return 0; } |
У прикладі, наведеному вище, коли ptr1
виходить з області видимості, він не знищує Item, оскільки член m_ptr
класу Item також володіє Item-ом. Таким чином, не залишається нікого, хто міг би видалити Item (m_ptr
ніколи не виходить з області видимості, тому він цього не зробить).
Результат виконання програми:
Item acquired
Розумний вказівник std::weak_ptr
Розумний вказівник std::weak_ptr був розроблений для вирішення вищеописаної проблеми «циклічної залежності». std::weak_ptr є спостерігачем — він може спостерігати і отримувати доступ до того ж об’єкту, на який вказує std::shared_ptr (або інший std::weak_ptr), але не рахуватися власником цього об’єкта. Пам’ятайте, коли std::shared_ptr виходить з області видимості, він перевіряє, чи є інші власники std::shared_ptr. std::weak_ptr власником не рахується!
Давайте перепишемо першу програму цього уроку, але вже з використанням std::weak_ptr:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
#include <iostream> #include <memory> // для std::shared_ptr і std::weak_ptr #include <string> class Human { std::string m_name; std::weak_ptr<Human> m_partner; // зверніть увагу, тут std::weak_ptr public: Human(const std::string &name): m_name(name) { std::cout << m_name << " created\n"; } ~Human() { std::cout << m_name << " destroyed\n"; } friend bool partnerUp(std::shared_ptr<Human> &h1, std::shared_ptr<Human> &h2) { if (!h1 || !h2) return false; h1->m_partner = h2; h2->m_partner = h1; std::cout << h1->m_name << " is now partnered with " << h2->m_name << "\n"; return true; } }; int main() { auto anton = std::make_shared<Human>("Anton"); auto ivan = std::make_shared<Human>("Ivan"); partnerUp(anton, ivan); return 0; } |
Результат виконання програми:
Anton created
Ivan created
Anton is now partnered with Ivan
Ivan destroyed
Anton destroyed
Функціонально все працює майже ідентично програмі, наведеній на початку цього уроку. Однак тепер, коли ivan
виходить з області видимості, він бачить, що немає іншого std::shared_ptr, який вказує на Ivan
(std::weak_ptr з Anton
не рахується). Тому він знищує Ivan
. Те ж саме відбувається і з Anton
.
Використання розумного вказівника std::weak_ptr
Недоліком розумного вказівника std::weak_ptr є те, що його не можна використовувати напряму (немає оператора ->
). Щоб використовувати std::weak_ptr, ви спочатку повинні конвертувати його в std::shared_ptr (за допомогою методу lock()), а потім вже використовувати std::shared_ptr. Наприклад, перепишемо вищенаведену програму:
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 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
#include <iostream> #include <memory> // для std::shared_ptr і std::weak_ptr #include <string> class Human { std::string m_name; std::weak_ptr<Human> m_partner; // зверніть увагу, тут std::weak_ptr public: Human(const std::string &name) : m_name(name) { std::cout << m_name << " created\n"; } ~Human() { std::cout << m_name << " destroyed\n"; } friend bool partnerUp(std::shared_ptr<Human> &h1, std::shared_ptr<Human> &h2) { if (!h1 || !h2) return false; h1->m_partner = h2; h2->m_partner = h1; std::cout << h1->m_name << " is now partnered with " << h2->m_name << "\n"; return true; } const std::shared_ptr<Human> getPartner() const { return m_partner.lock(); } // використовуємо метод lock() для конвертації std::weak_ptr в std::shared_ptr const std::string& getName() const { return m_name; } }; int main() { auto anton = std::make_shared<Human>("Anton"); auto ivan = std::make_shared<Human>("Ivan"); partnerUp(anton, ivan); auto partner = ivan->getPartner(); // передаємо для partner вміст розумного вказівника, яким володіє ivan std::cout << ivan->getName() << "'s partner is: " << partner->getName() << '\n'; return 0; } |
Результат виконання програми:
Anton created
Ivan created
Anton is now partnered with Ivan
Ivan's partner is: Anton
Ivan destroyed
Anton destroyed
Нам не потрібно турбуватися про циклічну залежність зі змінною partner
(типу std::shared_ptr), оскільки вона є простою локальною змінною всередині функції main() і знищується при завершенні виконання функції main().
Висновки
Розумний вказівник std::shared_ptr використовується для володіння одним динамічно виділеним ресурсом відразу декількома розумними вказівниками. Ресурс буде знищено, коли останній std::shared_ptr вийде з області видимості. std::weak_ptr використовується, коли потрібен розумний вказівник, який має доступ до ресурсу, але не вважається його власником.
Тест
Виправте вищенаведену програму зі спрощеною циклічною залежністю, щоб Item був коректно звільнений.
Відповідь
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <iostream> #include <memory> // для std::shared_ptr і std::weak_ptr class Item { public: std::weak_ptr<Item> m_ptr; // використовуємо std::weak_ptr, щоб m_ptr не підтримував Item-а Item() { std::cout << "Item acquired\n"; } ~Item() { std::cout << "Item destroyed\n"; } }; int main() { auto ptr1 = std::make_shared<Item>(); ptr1->m_ptr = ptr1; // m_ptr тепер є власником Item-а, членом якого він є сам return 0; } |