Українська
Розбори та типові помилки
Перевірка власника ресурсу
Для класу з буфером перевірте порожній об’єкт, незалежність копії, присвоєння між різними розмірами, самоприсвоєння, конструювання переміщенням, присвоєння переміщенням і повторне використання джерела. Якщо контракт обіцяє порожнє джерело, перевіряйте саме цю обіцянку.
Нульовий розмір потребує уваги: не виконуйте арифметику з нульовим вказівником навіть у начебто порожньому діапазоні. У прикладах копіювання виконується лише для додатного розміру, а доступ до елемента перевіряє індекс. Для матриці треба перевірити добуток розмірностей до виділення пам’яті.
Перевірки assert виявляють логічні помилки конкретних сценаріїв. Вони не доводять відсутність усіх витоків і не моделюють автоматично відмову new. Для поглибленого досліду додають керовану точку відмови алокації або аналізатор пам’яті; результати такого досліду слід відрізняти від звичайного запуску з невеликими масивами.
Найкраща кінцева перевірка дизайну – чи можна замінити ручний буфер на vector без зміни публічного інтерфейсу. Якщо так, предметна логіка відокремлена від деталей володіння, і правило нуля стає природним завершенням навчального прикладу.
Покроковий аудит присвоєння власного буфера
Нехай a володіє трьома елементами, а b – п’ятьма. Для a = b не можна спершу звільнити буфер a, а потім лише спробувати отримати новий: якщо виділення завершиться винятком, старе значення вже втрачено. Це може бути допустимим лише за чітко заявленої слабшої гарантії, але в короткій навчальній реалізації простіше забезпечити сильнішу.
Copy-and-swap розбиває операцію на підготовку й підтвердження. Спочатку створюється temp, незалежна копія b. У цей момент a і b незмінні. Потім безвинятковий swap передає новий буфер a, а старий буфер a переходить у temp. Нарешті деструктор temp звільняє старий ресурс. Ніхто не має двох обов’язків звільнення тієї самої адреси.
Для a = a тимчасова копія теж незалежна: новий буфер створено до звільнення старого. Окрема перевірка адрес може зекономити копію, але для правильності цієї реалізації вона не обов’язкова. Для самопереміщення ситуація інша: занулення джерела та звільнення цілі стосуються одного об’єкта, тому приклад має явну перевірку this != &x.
Під час аудиту корисно уявно підписати кожну адресу: P належить a, Q належить b, R належить temp. Після swap власником R стає a, P – temp, Q лишається b. Після завершення присвоєння P звільнено, R і Q живі. Цей простий облік точніше виявляє подвійне звільнення, ніж перегляд лише значень елементів у налагоджувачі.
Матриця сценаріїв для правила п’яти
Перевірка спеціальних функцій має охоплювати не лише розміри, а й відношення між джерелом та ціллю. Різні об’єкти й один об’єкт дають різні ризики. Порожній ресурс додає ще один вимір перевірки.
| Дія | Вхідний стан | Перевірка після дії |
|---|---|---|
| Copy constructor | Непорожнє джерело | Дані рівні, сховища незалежні |
| Copy assignment | Різні розміри | Стара ціль звільнена, джерело незмінне |
| Self-copy | Той самий об’єкт | Значення й інваріант збережено |
| Move constructor | Непорожнє джерело | Ресурс у цілі, джерело за контрактом |
| Move assignment | Ціль уже має ресурс | Старий ресурс цілі звільнено один раз |
| Self-move | Той самий об’єкт | Явно заявлений коректний стан |
| Copy empty | Нульова довжина | Немає доступу до неіснуючого елемента |
Для перевірки незалежності недостатньо порівняти два масиви одразу після копії: поверхнева копія теж дасть однакові значення. Треба змінити одну копію й перевірити іншу. Для RAII-токена навпаки: копіювання має бути недоступним на етапі компіляції, а переміщення повинно залишити лише одного активного власника.
Тести часу життя не повинні читати вже звільнену пам’ять «для перевірки». Натомість спостерігайте за безпечним зовнішнім лічильником або журналом володіння, який живе довше за тестовані об’єкти. Не називайте відсутність аварії доказом: невизначена поведінка може випадково здаватися успішною.
Як вибрати ресурс для RAII-обгортки
Урок RAII не вимагає залежності від операційної системи. Тимчасове право доступу до навчального обладнання можна моделювати індексом в пулі та булевим станом зайнятості. Конструктор перевіряє вільність, а деструктор повертає індекс. Реальний системний дескриптор додає правила конкретного API, але не змінює основної ідеї власника.
Контракт переміщення такого токена має включати активність джерела. Після передачі воно не повинно повернути той самий індекс у пул. Тому джерело переводять у неактивний стан, а деструктор перевіряє ознаку активності. При присвоєнні переміщенням ціль спершу повертає власний старий ресурс, потім приймає новий.
Окремо перевіряють життя самого пулу. Якщо токен зберігає посилання на пул, пул має жити довше. RAII не усуває автоматично проблему недійсного посилання на власника іншого рівня. Можливе рішення – зовнішня область видимості пулу, всередині якої створюються всі токени; інші моделі потребують іншого явного механізму володіння.
Для локальної транзакції ресурсом є право вирішити, чи залишити зміну. Guard має початковий знімок і прапорець commit. Якщо операція складається з кількох чисел, відкат одного поля може бути недостатнім: потрібно відновити весь інваріант разом. У переказі між рахунками це, зокрема, сума двох балансів. Так поняття ресурсу поєднується з предметним контрактом, а не зводиться до пари new/delete.
Яке значення має залишитися після переміщення
Для власного рядка обрано сильну й просту обіцянку: джерело стає порожнім. Ця обіцянка не випливає із самого синтаксису T&&. Автор класу міг би залишити інший коректний стан, якщо він чітко описаний і деструктор може безпечно завершити його життя. Саме контракт визначає, що тест має перевіряти.
У стандартного контейнера не слід перевіряти конкретну місткість після переміщення, якщо документація цього не обіцяє. Навіть коли спостереження в одному компіляторі показує нуль, це не перетворює деталь реалізації на переносиму властивість програми. Натомість присвойте джерелу нове значення та перевірте подальшу роботу.
Використання джерела після переміщення не завжди помилка. Помилка виникає, коли код покладається на старе значення або порушує передумову операції. Виклик empty може бути допустимим, а доступ до першого елемента без перевірки порожності – ні. Аналізатор попереджень допомагає знайти підозріле місце, але остаточне рішення спирається на контракт типу.
Перевірка строгої гарантії без запуску невизначеної поведінки
Для copy-and-swap точкою відмови є створення тимчасової копії. Якщо новий буфер не отримано, swap ще не викликано, тому ціль незмінна. Це логічне обґрунтування шляху виконання. Звичайний тест на маленькому масиві не змушує систему повернути bad_alloc і не повинен заявляти, що такий шлях був фактично виконаний.
У поглибленій вправі можна додати керований навчальний лічильник, який кидає виняток перед алокацією певної копії. Тоді тест спочатку запам’ятовує значення цілі, активує відмову, виконує присвоєння й перевіряє рівність зі знімком. Це не тест реальної нестачі пам’яті, але він перевіряє саме обраний шлях відмови.
Не моделюйте помилку подвійним delete або записом після звільнення. Такі дії самі є невизначеною поведінкою, тож результат тесту не має надійного змісту. Для перевірки кількості звільнень використовуйте зовнішній безпечний лічильник або спеціалізований інструмент, який спостерігає за коректно виконуваною програмою.
Від локального guard до складеної операції
Одна транзакція може змінювати кілька полів. Наприклад, переказ списує гроші з одного балансу й додає до іншого. Якщо зберегти знімок тільки першого, відкат після додавання не відновить суму системи. Спочатку потрібно визначити повний інваріант, а потім набір станів, які мають повертатися разом.
Для двох цілих чисел знімок дешевий, а відновлення не кидає винятків. Для двох великих контейнерів знімок може бути дорогим і сам завершитися помилкою. Його готують до зміни оригіналів. Підтвердження має бути коротким і безвинятковим, наприклад обміном готових станів із тимчасовими об’єктами.
Після commit новий стан є остаточним у межах цієї локальної моделі. Якщо далі виконується друк звіту й він не вдався, це не обов’язково причина відкочувати предметну операцію. Визначте межу транзакції: що входить у цілісну зміну, а що є лише повідомленням про вже завершений результат.