Українська
Моделі володіння та span
Невласницький інтерфейс і span
Правило C++ Core Guidelines: звичайні сирі вказівники зазвичай позначають невласницький доступ. Якщо функція приймає const Node*, вона може читати вузол, але не повинна delete його без окремо визначеного контракту власності. Передавання unique_ptr за значенням натомість виражає передачу виключного володіння.
std::span<T> з <span> поєднує адресу й довжину послідовності без володіння. Він зручніший за пару вказівник/розмір, але також не подовжує час життя. Span на буфер вектора перестає бути чинним після перерозподілу. Його наявність не робить довільний індекс коректним автоматично.
Вибір моделі володіння
Спочатку запитайте, чи потрібне окреме динамічне виділення. Звичайна локальна структура часто достатня. Якщо потрібна послідовність, виберіть vector. Якщо один об’єкт має передаватися між власниками, виберіть unique_ptr. Якщо незалежні частини системи справді повинні спільно продовжувати життя об’єкта, розгляньте shared_ptr.
Спостерігач не повинен випадково ставати власником. Наприклад, посилання дочірнього вузла дерева на батька може бути сирим невласницьким pointer, якщо батько гарантовано живе довше. Якщо батько керується спільним володінням і може зникати незалежно, потрібний weak_ptr. Не можна створити weak_ptr без shared-власника лише за сирою адресою.
Тестування володіння перевіряє також видалення й передачу, а не лише створення. Створіть порожній список, один вузол, кілька вузлів; вилучіть перший, останній та всі. Після кожної дії перевірте доступні дані й відсутність зайвого власника. Санітайзер запускають на тих самих сценаріях, але не вважають замінником моделі.
Розбір графа володіння на конкретних сценаріях
Розгляньмо каталог документів. Є один кореневий каталог, він містить дочірні каталоги, а ті – ще нижчі. Якщо корінь знищується, усе його піддерево також має зникнути. Ця вимога природно відповідає unique_ptr для дочірніх вузлів. Кожний вузол має рівно одного батька-власника. Зворотний зв’язок із дитини до батька потрібний для навігації, але не повинен утримувати батька від знищення.
На схемі малюйте суцільну стрілку для володіння і пунктирну для спостереження. Потім подумки вилучіть кожний зовнішній власник і визначте, які об’єкти зникнуть. Якщо залишається замкнений цикл суцільних shared-стрілок, лічильники не обнуляться. Якщо спостерігач використовується після зникнення його цілі, модель має порушення часу життя. Саме ці питання потрібно вирішити до написання delete.
Таблиця 5.1. Вибір типу за вимогами до життя об’єктів
| Вимога | Придатна модель |
|---|---|
| Один локальний запис | Звичайний об’єкт за значенням; окреме виділення не потрібне. |
| Послідовність значень | vector володіє елементами та буфером. |
| Дитина належить одному батькові | unique_ptr на дитину, невласницький доступ назад. |
| Пісня потрібна двом незалежним плейлистам | shared_ptr на спільну пісню. |
| Список нещодавніх ресурсів не повинен утримувати їх | weak_ptr з перевіркою lock. |
| Функція тимчасово читає діапазон | span або константне посилання з чинним власником. |
Спільне володіння не означає, що кожна функція повинна приймати shared_ptr. Якщо функція лише друкує назву пісні під час виклику, достатньо const Song&. Параметр shared_ptr за значенням доречний, коли функція має зберегти власну частку володіння. Інакше інтерфейс без потреби прив’язує просте читання до конкретного способу керування пам’яттю.
Так само unique_ptr<T>& означає, що функція може змінити самого власника, наприклад замінити голову списку. T& означає доступ до наявного об’єкта без передачі власності. unique_ptr<T> за значенням означає, що викликач передає ресурс. Різниця між цими інтерфейсами важливіша за кількість символів у сигнатурі.
Що насправді змінює переміщення
Нехай first володіє вузлом, а observer=first.get() читає його. Після second=std::move(first) вузол не обов’язково змінює адресу: передано відповідальність за нього. first порожній,second є власником, observer може залишатися чинним доти, доки second не знищить або не замінить ресурс. Це пояснює, чому переміщення unique_ptr у векторі не рухає самі динамічні вузли дерева.
Але якщо second уже володів іншим вузлом, присвоєння знищить його попередній ресурс. Спостерігачі на старий ресурс second стануть висячими. Тому фраза «move не змінює адресу» без уточнення небезпечна: потрібно запитати, який саме об’єкт і якого власника розглядаємо.
std::move сам не виконує магічне копіювання байтів і не звільняє пам’ять. Він дозволяє обрати операцію, яка може забрати ресурс. Для unique_ptr правила однозначні, а для власних типів відповідну поведінку визначає їхня реалізація. Детальне дослідження цих операцій належить темі 8.