Українська
Категорії значень і copy-and-swap
Категорії значень і посилання
Категорія належить виразу, а не просто типу змінної. lvalue позначає об’єкт з ідентичністю; ім’я локальної змінної є lvalue. prvalue описує обчислене значення, наприклад літерал 42 або створення T{}. xvalue позначає об’єкт, ресурс якого можна використати повторно, наприклад результат std::move(a).
Назви glvalue і rvalue групують категорії: glvalue включає lvalue та xvalue, rvalue включає prvalue та xvalue. Тому схема має спільний вузол xvalue, а не дві різні категорії з однаковим ім’ям. Ця класифікація пояснює, чому об’єкт може одночасно мати ідентичність та бути кандидатом для перенесення ресурсу.
Параметр T&& other має тип rvalue-посилання, але вираз other усередині функції є lvalue, оскільки він має ім’я. Для передачі його ресурсу далі потрібен std::move(other). З іншого боку, переміщення з const T часто вибирає копіювання: звичайний T&& не може дозволити зміну константного джерела.
Не слід розставляти std::move біля кожного повернення чи аргументу. Після передачі ресурсу старе значення може бути втрачено, а примусове перетворення локального результату інколи заважає оптимізації. Спочатку встановіть право володіння та потребу в копії, потім обирайте механізм передачі.
Copy-and-swap і гарантія винятків
Ідіома copy-and-swap спочатку створює тимчасову копію, а потім обмінює ресурси з цільовим об’єктом. Якщо копіювання не вдалося, старий стан цілі не змінювався. Якщо обмін не кидає винятків, успішний перехід є коротким і надійним. Старий ресурс знищить тимчасовий об’єкт після завершення операції.
Строга гарантія означає, що відхилена операція не змінює спостережуваний стан. Базова гарантія означає збереження інваріантів і відсутність витоку, але значення могло змінитися. Гарантія відсутності винятків сильніша щодо виходу з операції, але сама по собі не каже, чи результат математично правильний. Назву гарантії слід прив’язувати до конкретного методу.
Для копіювання буфера тимчасова копія коштує пам’яті. Це прийнятна плата за простий контракт, але не універсальний максимум швидкодії. Оптимізований клас може повторно використати місткість цілі; тоді доведеться окремо довести поведінку при частковій помилці.
std::exchange(other.data_, nullptr) повертає попереднє значення й одночасно встановлює нове. Це зручний запис перенесення вказівника, а не автоматичне звільнення ресурсу. У присвоєнні переміщенням спочатку потрібно обробити власний ресурс цілі та самопереміщення.
Повернення значень і усунення копій
Повернення об’єкта за значенням не означає обов’язкову дорогу копію. У відповідних випадках prvalue C++17 результат створюється одразу в кінцевому місці. NRVO для іменованої локальної змінної є окремою дозволеною оптимізацією, яку не слід вважати безумовною для кожного коду.
Звичайний запис return result; залишає компілятору можливість NRVO та передбачені мовою механізми переміщення. Запис return std::move(result); може прибрати умову для NRVO. Не потрібно «допомагати» компілятору таким способом без конкретного обґрунтування й вимірювання.
Трасувальний деструктор може зробити послідовність повідомлень залежною від дозволеної оптимізації. Тому тестувати треба насамперед значення й інваріант, а не універсальне число копій для будь-якого компілятора. У прикладі vector::reserve ми перевіряємо відому конфігурацію MSVC та явно записуємо її в маніфесті.
Простий тест продуктивності також може вводити в оману: час виділення пам’яті, розмір даних, оптимізація й прогрівання впливають на результат. Підрахунок операцій пояснює механіку, а вимірювання часу відповідає іншому запитанню. Для маленьких типів переміщення може коштувати стільки ж, скільки копіювання.
RAII для стану та транзакції
Тимчасова зміна параметрів std::cout теж є ресурсом: старі flags, precision і fill треба повернути. Обгортка зберігає їх у конструкторі та відновлює в деструкторі. Приклад використовує ostringstream, щоб перевірити результат без залежності від консолі чи Windows API.
Транзакційна обгортка зберігає попередній стан і має прапорець підтвердження. До commit() деструктор виконує відкат; після нього залишає новий стан. Така локальна транзакція не є транзакцією бази даних: вона не забезпечує стійкості після аварії та ізоляції від потоків.
Відкат має бути надійним. Присвоєння простого int не кидає винятків, але копіювання великого vector може виділяти пам’ять. Для складних структур старий стан часто готують наперед і відновлюють через безвинятковий обмін. Вибір механізму залежить від контракту даних.
Scope guard тримає невласне посилання на стан; стан має жити довше за guard. Копіювання забороняється, інакше два деструктори могли б відкотити одну зміну двічі. У простому прикладі також забороняємо переміщення: воно не потрібне сценарію й ускладнило б право на відкат.