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

  Юрій  | 

  Оновл. 8 Жов 2021  | 

 200

Тепер, коли ми розглянули основи семантики переміщення, ми можемо повернутися до теми розумних вказівників.

Розумні вказівники

На попередніх уроках ми говорили про те, як використання вказівників може призвести до помилок і витоків пам’яті в деяких ситуаціях. Наприклад, при передчасному завершенні функції або при генерації винятку, коли вказівник може бути видалений неналежним чином:

Розумний вказівник — це клас, який управляє динамічно виділеною пам’яттю/ресурсом/об’єктом. Головна фішка розумних вказівників полягає в управлінні та забезпеченні коректного видалення динамічно виділеного ресурсу у відповідний час (зазвичай, коли розумний вказівник виходить з області видимості).

Через це розумні вказівники ніколи не можна виділяти динамічно (в іншому випадку, існує ризик того, що розумний вказівник буде неправильно видалений, це означає, що ресурс, який йому належить, також не буде видалений, і станеться витік пам’яті). Завжди виділяючи розумні вказівники статичним способом (як локальні змінні), ви отримуєте гарантію, що розумний вказівник коректно вийде з області видимості і видалить об’єкт, який зберігається.

Стандартна бібліотека в 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:

Коли std::unique_ptr виходить з області видимості, він видаляє Item, яким володіє.

На відміну від std::auto_ptr, std::unique_ptr коректно реалізовує семантику переміщення:

Результат виконання програми:

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 володіє ресурсом. Наприклад:

Результат:

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(). Це шаблон функції, який створює об’єкт типу шаблону і ініціалізує його аргументами, переданими в функцію. Наприклад:

Результат виконання програми:

7/9
0/1

Використання функції std::make_unique() є необов’язковим, але рекомендується замість використання розумного вказівника std::unique_ptr. Справа в простоті. Крім того, std::make_unique() вирішує проблему безпеки використання винятків, яка може виникнути в результаті невизначеного порядку обробки аргументів функції (оскільки мова С++ явно не вказує цей порядок).

Правило: Використовуйте функцію std::make_unique() замість створення розумного вказівника std::unique_ptr і використання оператора new.

Безпека використання винятків

Ось приклад для тих, хто не зрозумів, що це за проблема з безпекою використання винятків, яка прозвучала вище:

Тут компілятору надається велика гнучкість при обробці виклику функції. Він може спочатку виділити новий 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 можна повернути з функції по значенню:

Тут createItem() повертає std::unique_ptr по значенню. Якщо значення, що повертається, не присвоюється якомусь об’єкту, то воно виходить з області видимості, і Item видаляється. Якщо значення присвоюється об’єкту (як показано в функції main()), то за допомогою семантики переміщення Item переміщується зі значення, що повертається, в потрібний об’єкт (в даному випадку в ptr). Це робить повернення ресурсів за допомогою std::unique_ptr набагато безпечнішим, ніж повернення «необроблених» вказівників!

Загалом, ви не повинні повертати std::unique_ptr по адресі (взагалі) або по посиланню (якщо у вас немає на це вагомих причин).

Передача розумного вказівника std::unique_ptr в функцію

Якщо ви хочете, щоб функція стала власником вмісту розумного вказівника, то передавати std::unique_ptr в функцію потрібно по значенню. Зверніть увагу, оскільки семантика копіювання відключена, то вам доведеться використати std::move() для фактичної передачі ресурсу в функцію:

Результат виконання програми:

Item acquired
I am an Item!
Item destroyed
Ending program

Зверніть увагу, в даному випадку право власності на Item було передано в takeOwnership(), тому Item знищується в кінці takeOwnership(), а не в кінці main().

Однак в більшості випадків вам не потрібно буде, щоб функція володіла ресурсом. Хоча ви можете передати std::unique_ptr по посиланню (що дозволить функції використовувати об’єкт без передачі їй права власності на цей об’єкт), ви повинні робити це тільки тоді, коли caller може змінити переданий об’єкт.

Замість цього краще передавати сам об’єкт по адресі або по посиланню (в залежності від того, чи є null допустимим аргументом). Це дозволить функції залишатися осторонь від управління об’єктом. Щоб отримати необроблений вказівник на об’єкт з std::unique_ptr, ми можемо використати метод get():

Результат виконання програми:

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, обидва з яких легко уникнути. По-перше, не дозволяйте декільком класам «володіти» одним і тим же ресурсом. Наприклад:

Хоча це синтаксично допустимо, кінцевим результатом буде те, що і item1, і item2 спробують видалити Item, що призведе до невизначеної поведінки/результатів.

По-друге, не видаляйте виділений ресурс вручну з-під std::unique_ptr:

Якщо ви це зробите, std::unique_ptr спробує видалити вже видалений ресурс, що знову ж таки призведе до невизначеної поведінки/результатів.

Зверніть увагу, функція std::make_unique() запобігає ненавмисному виникненню обох вищенаведених ситуацій.

Тест

Завдання №1

Якщо у вашому класі є розумний вказівник в якості члена вашого класу, то чому ви повинні намагатися уникати динамічного виділення об’єктів цього класу?

Відповідь №1

Розумні вказівники в якості членів класу видаляють свій ресурс тільки в тому випадку, якщо об’єкт класу виходить з області видимості. Якщо ми виділимо об’єкт класу динамічно і не видалимо його належним чином, то об’єкт класу ніколи не вийде з області видимості, і розумний вказівник не зможе очистити ресурс, який він зберігає.

Завдання №2

Змініть наступну програму, замінивши звичайний вказівник на розумний вказівник std::unique_ptr, де це необхідно:

Відповідь №2

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

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

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

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