Що ви опануєте
Ви навчитеся пояснювати семантику копіювання й переміщення, застосовувати RAII, правило нуля та алгоритми стандартної бібліотеки. Зможете обґрунтувати тип володіння й описати стан після передачі ресурсу. Приклад — колекція навчальних результатів та файл із підсумком.
Семантика значення й ресурсу
Копіювання створює окремий об’єкт із визначеною типом семантикою. Для std::vector<int> новий vector має власні елементи. Копія сирого вказівника повторює адресу й не створює новий керований об’єкт. Переміщення використовує операцію типу, яка може передати ресурс замість копіювання його вмісту. Сам вираз std::move лише дозволяє вибір такої операції.
RAII пов’язує ресурс із часом життя власника. Приклад ресурсу — пам’ять, файл або блокування. Деструктор виконує звільнення за звичайного завершення й розкручування стека. Файловий потік std::ofstream автоматично закриває файл, але помилки запису та закриття потребують окремої уваги до стану потоку.
Правило нуля та спеціальні операції
Клас із полями string і vector часто не потребує ручних операцій копіювання, переміщення й деструктора. Це правило нуля: ресурсні члени забезпечують потрібну поведінку. Якщо клас безпосередньо керує сирим ресурсом, необхідно узгодити всі спеціальні операції. Ручний деструктор поряд із неузгодженим копіюванням може спричинити подвійне звільнення.
Поле unique_ptr робить тип некопійованим за стандартною поведінкою, оскільки один ресурс має одного власника. За потреби незалежної копії визначають осмислену операцію клонування. shared_ptr виражає спільне володіння; він має витрати лічильника й ризик циклів. Його вибирають через реальну спільну відповідальність, а не для уникнення аналізу часу життя.
Виконуваний приклад колекції
#include <algorithm>
#include <iostream>
#include <numeric>
#include <string>
#include <utility>
#include <vector>
struct Results {
std::string course;
std::vector<int> scores;
};
int main() {
Results original{"C++", {70, 90, 60}};
Results copy = original; // Окремий vector.
copy.scores[0] = 100;
Results transferred = std::move(copy);
std::sort(transferred.scores.begin(), transferred.scores.end());
const int sum = std::accumulate(transferred.scores.begin(),
transferred.scores.end(), 0);
std::cout << original.scores[0] << " " << sum << '\n'; // 70 250.
}
Results складається з ресурсних типів і використовує автоматично визначені операції. Зміна копії не змінює original. Після переміщення transferred містить переданий результат; у copy залишається коректний, але для таких стандартних типів загалом не визначений конкретний вміст. Його можна знищити або присвоїти нове значення; не покладайтеся на старі елементи.
Контейнери, ітератори та алгоритми
STL історично охоплює контейнери, ітератори й алгоритми стандартної бібліотеки. Контейнер керує набором елементів. Ітератор позначає позицію та спосіб доступу. Алгоритм працює з діапазоном, визначеним початком і кінцем. Кінець end() не є елементом для розіменування.
sort потребує ітераторів довільного доступу та коректного порядку порівняння. accumulate проходить діапазон і додає значення до початкового акумулятора. Тип початкового значення впливає на тип обчислення. Для великих сум 0LL може задати ширший цілий тип, але потрібно перевірити діапазон і цього типу. Узагальнений алгоритм не скасовує числових обмежень.
Шаблони
Шаблон описує сімейство функцій або класів для різних типів. Компілятор створює потрібні спеціалізації за використанням. Наприклад, std::vector<int> і std::vector<std::string> мають однакові принципи інтерфейсу, але різні елементи. Контракт шаблону повинен описувати потрібні операції типу.
#include <concepts>
template<std::integral T>
T doubled(T value) {
return value + value; // Контракт: результат має вкладатися в T.
}
Фрагмент використовує C++20 concept integral, який обмежує типи цілочисельною категорією. Це перевіряє типову вимогу, але не захищає від переповнення. Перед використанням потрібно забезпечити допустимий діапазон або додати явну перевірку. Коментар контракту не замінює перевірки даних від непідконтрольного джерела.
RAII для файлу
#include <fstream>
#include <stdexcept>
void save_summary(int sum) {
std::ofstream output{"summary.txt"};
if (!output) throw std::runtime_error("open failed");
output << sum << '\n';
output.close(); // Перевіряємо також результат завершення запису.
if (!output) throw std::runtime_error("write failed");
}
Функція відкриває файл у робочій папці й перевіряє стан. За звичайного виходу RAII звільнить ресурс, навіть якщо код зміниться. Явне закриття тут дозволяє перевірити помилку завершення запису до повернення. Перезапис потрібного файлу має бути погодженим контрактом; для критичних даних використовують окрему стратегію атомарного збереження.
Типові помилки та практика
Поширені проблеми: використання переміщеного вмісту без гарантії, випадкове копіювання великої колекції, розіменування end, некоректний компаратор і зміна vector під час роботи зі старими ітераторами. std::move для const об’єкта часто веде до копіювання, оскільки ресурс неможливо забрати через const контракт.
Практичний аналіз
Поясніть зміну original, copy та transferred після кожного рядка прикладу. Додайте тест порожнього vector і перевірте суму. Який контракт потрібний для сортування за власним полем? Чому автоматичне закриття файлу не є доказом успішного запису? Відповіді повинні відокремлювати звільнення ресурсу від результату операції.
Гарантії ресурсного класу та переміщення
RAII пов’язує ресурс із життям об’єкта. Для файлу ресурсом є відкритий дескриптор, для unique_ptr — керований динамічний об’єкт. Якщо локальний власник залишає область через return або виняток, його деструктор виконує звільнення. Це дозволяє аналізувати ресурс за структурою коду й межами життя.
Після переміщення стандартний об’єкт залишається у допустимому стані, але конкретне значення часто не визначене контрактом. Для std::string не слід загалом очікувати порожній рядок після move. Його можна знищити або надати нове значення; інші операції мають дотримуватися власних передумов. std::move сам створює вираз для вибору відповідної операції, а фактичну дію виконує конструктор чи оператор.
Rule of Zero пропонує будувати класи з ресурсних компонентів, які вже правильно керують життям. Тоді власні деструктори та операції копіювання часто не потрібні. Якщо клас безпосередньо керує особливим ресурсом, визначте політику копіювання, переміщення та відмови цілісно. Випадкова поверхнева копія raw-власника може спричинити повторне звільнення.
Для STL-алгоритму перевіряйте вимоги до даних і функції порівняння. Компаратор sort повинен задавати строге слабке впорядкування; використання <= замість < може порушити контракт. Якщо накопичуєте цілі значення, тип початкового акумулятора впливає на тип обчислення й ризик переповнення. Узагальнений код потребує таких самих предметних меж, як і звичайна функція.
Схема процесу
Тип пов’язує ресурс із часом життя.
Самоперевірка
Як обґрунтувати відповідь своїми словами?
Підсумок
RAII та правило нуля допомагають зробити ресурси частиною типів. Копіювання й переміщення мають різну семантику, яку потрібно пояснювати. Узагальнені алгоритми залишають вимоги до діапазонів, порівняння й числових меж.