Тепер, коли ми розглянули основи семантики переміщення, ми можемо повернутися до теми розумних вказівників.
- Розумні вказівники
- Розумний вказівник std::unique_ptr
- Доступ до об’єкту, який зберігає розумний вказівник
- Розумний вказівник std::unique_ptr і динамічні масиви
- Функція std::make_unique()
- Безпека використання винятків
- Повернення розумного вказівника std::unique_ptr з функції
- Передача розумного вказівника std::unique_ptr в функцію
- Розумний вказівник std::unique_ptr і класи
- Неправильне використання розумного вказівника std::unique_ptr
- Тест
Розумні вказівники
На попередніх уроках ми говорили про те, як використання вказівників може призвести до помилок і витоків пам’яті в деяких ситуаціях. Наприклад, при передчасному завершенні функції або при генерації винятку, коли вказівник може бути видалений неналежним чином:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> void someFunction() { Item *ptr = new Item; int a; std::cout << "Enter an integer: "; std::cin >> a; if (a == 0) throw 0; // у випадку генерації винятку функція завершить своє виконання достроково, і ptr не буде видалено! // Робимо що-небудь з ptr тут delete ptr; } |
Розумний вказівник — це клас, який управляє динамічно виділеною пам’яттю/ресурсом/об’єктом. Головна фішка розумних вказівників полягає в управлінні та забезпеченні коректного видалення динамічно виділеного ресурсу у відповідний час (зазвичай, коли розумний вказівник виходить з області видимості).
Через це розумні вказівники ніколи не можна виділяти динамічно (в іншому випадку, існує ризик того, що розумний вказівник буде неправильно видалений, це означає, що ресурс, який йому належить, також не буде видалений, і станеться витік пам’яті). Завжди виділяючи розумні вказівники статичним способом (як локальні змінні), ви отримуєте гарантію, що розумний вказівник коректно вийде з області видимості і видалить об’єкт, який зберігається.
Стандартна бібліотека в C++11 має 4 класи розумних вказівників:
std::auto_ptr (який не слід використовувати — він видалений в C++17);
std::unique_ptr;
std::shared_ptr;
std::weak_ptr.
Розумний вказівник std::unique_ptr, по суті, є найбільш часто використовуваним класом розумного вказівника, тому спочатку розглянемо саме його. На наступних уроках розглянемо розумні вказівники std::shared_ptr і std::weak_ptr.
Розумний вказівник std::unique_ptr
Розумний вказівник std::unique_ptr є заміною std::auto_ptr в C++11. Ви повинні використовувати саме його для управління будь-яким динамічно виділеним об’єктом/ресурсом, але за умови, що std::unique_ptr повністю володіє переданим йому об’єктом, а не ділиться «володінням» ще з іншими класами. Розумний вказівник std::unique_ptr знаходиться в заголовку memory.
Розглянемо простий приклад використання std::unique_ptr:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> #include <memory> // для std::unique_ptr class Item { public: Item() { std::cout << "Item acquired\n"; } ~Item() { std::cout << "Item destroyed\n"; } }; int main() { // Виділяємо об'єкт класу Item і передаємо право власності на нього std::unique_ptr std::unique_ptr<Item> item(new Item); return 0; } // item виходить з області видимості тут, відповідно, Item знищується також тут |
Коли std::unique_ptr виходить з області видимості, він видаляє Item, яким володіє.
На відміну від std::auto_ptr, std::unique_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 |
#include <iostream> #include <memory> // для std::unique_ptr class Item { public: Item() { std::cout << "Item acquired\n"; } ~Item() { std::cout << "Item destroyed\n"; } }; int main() { std::unique_ptr<Item> item1(new Item); // виділення Item std::unique_ptr<Item> item2; // присвоюється значення nullptr std::cout << "item1 is " << (static_cast<bool>(item1) ? "not null\n" : "null\n"); std::cout << "item2 is " << (static_cast<bool>(item2) ? "not null\n" : "null\n"); // item2 = item1; // не скомпілюється: семантика копіювання відключена item2 = std::move(item1); // item2 тепер володіє item1, а для item1 присвоюється значення null std::cout << "Ownership transferred\n"; std::cout << "item1 is " << (static_cast<bool>(item1) ? "not null\n" : "null\n"); std::cout << "item2 is " << (static_cast<bool>(item2) ? "not null\n" : "null\n"); return 0; } // Item знищується тут, коли item2 виходить з області видимості |
Результат виконання програми:
Item acquired
item1 is not null
item2 is null
Ownership transferred
item1 is null
item2 is not null
Item destroyed
Оскільки std::unique_ptr розроблений з урахуванням семантики переміщення, то семантика копіювання за замовчуванням відключена. Якщо ви хочете передати вміст, керований std::unique_ptr, то ви повинні використовувати семантику переміщення. У програмі, наведеній вище, ми передаємо вміст std::unique_ptr за допомогою функції std::move() (яка конвертує item1
в r-value, що є тригером для виконання семантики переміщення замість семантики копіювання).
Доступ до об’єкту, який зберігає розумний вказівник
Розумний вказівник std::unique_ptr має перевантажені оператори *
і ->
, які використовуються для доступу до збережених об’єктів. Оператор *
повертає посилання на керований ресурс, а оператор ->
повертає вказівник.
Розумний вказівник std::unique_ptr не завжди може керувати об’єктом: або тому, що об’єкт був створений порожнім (з використанням конструктора за замовчуванням, або в об’єкт передано в якості параметра nullptr), або тому, що ресурс, яким він керував, був переміщений в інший std::unique_ptr. Тому, перш ніж використовувати будь-який з цих операторів, ви повинні перевірити, чи дійсно std::unique_ptr управляє ресурсом. На щастя, це легко зробити: std::unique_ptr має неявне перетворення в тип bool, повертаючи true
, якщо std::unique_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 |
#include <iostream> #include <memory> // для std::unique_ptr class Item { public: Item() { std::cout << "Item acquired\n"; } ~Item() { std::cout << "Item destroyed\n"; } friend std::ostream& operator<<(std::ostream& out, const Item &item) { out << "I am an Item!\n"; return out; } }; int main() { std::unique_ptr<Item> item(new Item); if (item) // використовуємо неявну конвертацію item в тип bool, щоб переконатися, що item володіє Item-ом std::cout << *item; // виводимо Item, яким володіє item return 0; } |
Результат:
Item acquired
I am an Item!
Item destroyed
У програмі, наведеній вище, ми використовуємо оператор *
для доступу до Item, яким володіє об’єкт item
класу std::unique_ptr, який ми потім виводимо за допомогою std::cout.
Розумний вказівник std::unique_ptr і динамічні масиви
На відміну від std::auto_ptr, std::unique_ptr досить розумний, щоб знати, коли використовувати одиничний оператор delete, а коли форму оператора delete для масиву, тому std::unique_ptr можна використовувати як з одиничними об’єктами, так і з динамічними масивами.
Однак використання std::vector майже завжди є кращим вибором, ніж використання std::unique_ptr з динамічним масивом.
Правило: Використовуйте std::vector замість використання розумного вказівника, який володіє динамічним масивом.
Функція std::make_unique()
У C++14 додали нову функцію — std::make_unique(). Це шаблон функції, який створює об’єкт типу шаблону і ініціалізує його аргументами, переданими в функцію. Наприклад:
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 |
#include <iostream> #include <memory> // для std::unique_ptr і std::make_unique class Fraction { private: int m_numerator = 0; int m_denominator = 1; public: Fraction(int numerator = 0, int denominator = 1) : m_numerator(numerator), m_denominator(denominator) { } friend std::ostream& operator<<(std::ostream& out, const Fraction &f1) { out << f1.m_numerator << "/" << f1.m_denominator; return out; } }; int main() { // Створюємо об'єкт з динамічно виділеним Fraction з numerator = 7 і denominator = 9 std::unique_ptr<Fraction> f1 = std::make_unique<Fraction>(7, 9); std::cout << *f1 << '\n'; // Створюємо об'єкт з динамічно виділеним масивом Fraction довжиною 5. // Використовуємо автоматичне визначення типу даних за допомогою ключового слова auto auto f2 = std::make_unique<Fraction[]>(5); std::cout << f2[0] << '\n'; return 0; } |
Результат виконання програми:
7/9
0/1
Використання функції std::make_unique() є необов’язковим, але рекомендується замість використання розумного вказівника std::unique_ptr. Справа в простоті. Крім того, std::make_unique() вирішує проблему безпеки використання винятків, яка може виникнути в результаті невизначеного порядку обробки аргументів функції (оскільки мова С++ явно не вказує цей порядок).
Правило: Використовуйте функцію std::make_unique() замість створення розумного вказівника std::unique_ptr і використання оператора new.
Безпека використання винятків
Ось приклад для тих, хто не зрозумів, що це за проблема з безпекою використання винятків, яка прозвучала вище:
1 |
some_function(std::unique_ptr<T>(new T), function_that_can_throw_exception()); |
Тут компілятору надається велика гнучкість при обробці виклику функції. Він може спочатку виділити новий T
, потім викликати function_that_can_throw_exception(), а потім вже створити std::unique_ptr, який керує динамічно виділеним T
. Якщо функція function_that_can_throw_exception() викине виняток, то виділений T
не буде коректно видалений, оскільки розумний вказівник, який повинен виконати його видалення, не встигне створитися. Це призведе до витоку пам’яті.
Функція std::make_unique() позбавлена цієї проблеми, оскільки виділення об’єкта T
і створення std::unique_ptr відбуваються всередині функції std::make_unique(), де порядок обробки аргументів чітко визначений.
Повернення розумного вказівника std::unique_ptr з функції
Розумний вказівник std::unique_ptr можна повернути з функції по значенню:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
std::unique_ptr<Item> createItem() { return std::make_unique<Item>(); } int main() { std::unique_ptr<Item> ptr = createItem(); // Робимо що-небудь return 0; } |
Тут createItem() повертає std::unique_ptr по значенню. Якщо значення, що повертається, не присвоюється якомусь об’єкту, то воно виходить з області видимості, і Item видаляється. Якщо значення присвоюється об’єкту (як показано в функції main()), то за допомогою семантики переміщення Item переміщується зі значення, що повертається, в потрібний об’єкт (в даному випадку в ptr
). Це робить повернення ресурсів за допомогою std::unique_ptr набагато безпечнішим, ніж повернення «необроблених» вказівників!
Загалом, ви не повинні повертати std::unique_ptr по адресі (взагалі) або по посиланню (якщо у вас немає на це вагомих причин).
Передача розумного вказівника std::unique_ptr в функцію
Якщо ви хочете, щоб функція стала власником вмісту розумного вказівника, то передавати std::unique_ptr в функцію потрібно по значенню. Зверніть увагу, оскільки семантика копіювання відключена, то вам доведеться використати std::move() для фактичної передачі ресурсу в функцію:
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 |
#include <iostream> #include <memory> // для std::unique_ptr class Item { public: Item() { std::cout << "Item acquired\n"; } ~Item() { std::cout << "Item destroyed\n"; } friend std::ostream& operator<<(std::ostream& out, const Item &item) { out << "I am an Item!\n"; return out; } }; void takeOwnership(std::unique_ptr<Item> item) { if (item) std::cout << *item; } // Item знищується тут int main() { auto ptr = std::make_unique<Item>(); // takeOwnership(ptr); // це не скомпілюється. Ми повинні використати семантику переміщення takeOwnership(std::move(ptr)); // використовуємо семантику переміщення std::cout << "Ending program\n"; return 0; } |
Результат виконання програми:
Item acquired
I am an Item!
Item destroyed
Ending program
Зверніть увагу, в даному випадку право власності на Item було передано в takeOwnership(), тому Item знищується в кінці takeOwnership(), а не в кінці main().
Однак в більшості випадків вам не потрібно буде, щоб функція володіла ресурсом. Хоча ви можете передати std::unique_ptr по посиланню (що дозволить функції використовувати об’єкт без передачі їй права власності на цей об’єкт), ви повинні робити це тільки тоді, коли caller може змінити переданий об’єкт.
Замість цього краще передавати сам об’єкт по адресі або по посиланню (в залежності від того, чи є null допустимим аргументом). Це дозволить функції залишатися осторонь від управління об’єктом. Щоб отримати необроблений вказівник на об’єкт з std::unique_ptr, ми можемо використати метод get():
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 |
#include <iostream> #include <memory> // для std::unique_ptr class Item { public: Item() { std::cout << "Item acquired\n"; } ~Item() { std::cout << "Item destroyed\n"; } friend std::ostream& operator<<(std::ostream& out, const Item &item) { out << "I am an Item!\n"; return out; } }; // Ця функція використовує тільки Item, тому ми передаємо вказівник на Item, а не посилання на весь std::unique_ptr<Item> void useItem(Item *item) { if (item) std::cout << *item; } int main() { auto ptr = std::make_unique<Item>(); useItem(ptr.get()); // примітка: Метод get() використовується для отримання вказівника на Item std::cout << "Ending program\n"; return 0; } // Item знищується тут |
Результат виконання програми:
Item acquired
I am an Item!
Ending program
Item destroyed
Розумний вказівник std::unique_ptr і класи
Звичайно, ви можете використовувати std::unique_ptr в якості члена композиції вашого класу. Таким чином, вам не потрібно буде турбуватися про те, чи видалить деструктор вашого класу ресурс std::unique_ptr, тому що std::unique_ptr буде автоматично знищений при знищенні об’єкта класу. Проте, якщо об’єкт вашого класу виділяється динамічно, то сам ресурс std::unique_ptr наражається на ризик неправильного видалення, і в такому випадку навіть розумний вказівник не допоможе.
Неправильне використання розумного вказівника std::unique_ptr
Існує два способи неправильного використання std::unique_ptr, обидва з яких легко уникнути. По-перше, не дозволяйте декільком класам «володіти» одним і тим же ресурсом. Наприклад:
1 2 3 |
Item *item = new Item; std::unique_ptr<Item> item1(item); std::unique_ptr<Item> item2(item); |
Хоча це синтаксично допустимо, кінцевим результатом буде те, що і item1
, і item2
спробують видалити Item, що призведе до невизначеної поведінки/результатів.
По-друге, не видаляйте виділений ресурс вручну з-під std::unique_ptr:
1 2 3 |
Item *item = new Item; std::unique_ptr<Item> item1(item); delete item; |
Якщо ви це зробите, std::unique_ptr спробує видалити вже видалений ресурс, що знову ж таки призведе до невизначеної поведінки/результатів.
Зверніть увагу, функція std::make_unique() запобігає ненавмисному виникненню обох вищенаведених ситуацій.
Тест
Завдання №1
Якщо у вашому класі є розумний вказівник в якості члена вашого класу, то чому ви повинні намагатися уникати динамічного виділення об’єктів цього класу?
Відповідь №1
Розумні вказівники в якості членів класу видаляють свій ресурс тільки в тому випадку, якщо об’єкт класу виходить з області видимості. Якщо ми виділимо об’єкт класу динамічно і не видалимо його належним чином, то об’єкт класу ніколи не вийде з області видимості, і розумний вказівник не зможе очистити ресурс, який він зберігає.
Завдання №2
Змініть наступну програму, замінивши звичайний вказівник на розумний вказівник std::unique_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 |
#include <iostream> class Fraction { private: int m_numerator = 0; int m_denominator = 1; public: Fraction(int numerator = 0, int denominator = 1) : m_numerator(numerator), m_denominator(denominator) { } friend std::ostream& operator<<(std::ostream& out, const Fraction &f1) { out << f1.m_numerator << "/" << f1.m_denominator; return out; } }; void printFraction(const Fraction* const ptr) { if (ptr) std::cout << *ptr; } int main() { Fraction *ptr = new Fraction(7, 9); printFraction(ptr); delete ptr; return 0; } |
Відповідь №2
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 |
#include <iostream> #include <memory> // для std::unique_ptr class Fraction { private: int m_numerator = 0; int m_denominator = 1; public: Fraction(int numerator = 0, int denominator = 1) : m_numerator(numerator), m_denominator(denominator) { } friend std::ostream& operator<<(std::ostream& out, const Fraction &f1) { out << f1.m_numerator << "/" << f1.m_denominator; return out; } }; // Ця функція використовує об'єкт класу Fraction, тому ми тільки його передаємо. // Таким чином ми можемо не турбуватися про те, який розумний вказівник використовує caller (якщо взагалі використовує) void printFraction(const Fraction* const ptr) { if (ptr) std::cout << *ptr; } int main() { auto ptr = std::make_unique<Fraction>(7, 9); printFraction(ptr.get()); return 0; } |