RAII в C++: почему ресурс живёт ровно столько, сколько объект
Утечки, двойное освобождение и забытый unlock — почти всегда следствие ручного управления ресурсами. RAII убирает эти классы ошибок целиком, привязывая владение ресурсом к времени жизни объекта.

Функция открывает файл, читает данные, по дороге кидает исключение — и файловый дескриптор остаётся висеть до конца процесса. Классическая утечка. В C с этим борются метками goto cleanup и дисциплиной, которую нарушают под дедлайн. В C++ проблема решается на уровне языка: ресурс захватывается в конструкторе и освобождается в деструкторе, а компилятор гарантирует вызов деструктора при выходе из области видимости — включая раскрутку стека при исключении. Это и есть RAII, и именно оно отличает C++ от языков со сборщиком мусора и от C.
Разберём, что даёт этот принцип на практике, где он ломается и почему std::unique_ptr — это не «умный указатель ради удобства», а прямое следствие модели владения.
Что означает RAII на самом деле
Название Resource Acquisition Is Initialization вводит в заблуждение: важна не столько инициализация, сколько детерминированное освобождение. Смысл в связке двух гарантий:
- объект захватывает ресурс в конструкторе — если конструктор бросил исключение, объект не считается созданным и деструктор для него не вызовется;
- объект освобождает ресурс в деструкторе — а деструктор для полностью сконструированного автоматического объекта вызывается всегда: и при нормальном выходе, и при раскрутке стека.
Под ресурсом понимается что угодно, требующее парного действия: память (new/delete), файл (open/close), мьютекс (lock/unlock), сокет, хендл базы данных, транзакция. Везде, где есть «захватил — освободи», есть место для RAII-обёртки.
Ключевая мысль: RAII превращает управление ресурсом из действия, которое можно забыть, в свойство типа, которое компилятор соблюдает за вас.
Разница видна на исключении
Возьмём мьютекс. Ручной вариант выглядит безобидно, пока между lock и unlock не появится ветка с исключением или ранним return.
#include <mutex>
std::mutex account_mutex;
// Опасный вариант: unlock может не выполниться
void transfer_manual(int amount) {
account_mutex.lock();
if (amount <= 0) {
return; // мьютекс остался заблокирован
}
do_transfer(amount); // если бросит — unlock не вызовется
account_mutex.unlock();
}
// RAII: lock_guard освобождает мьютекс в деструкторе
void transfer_raii(int amount) {
std::lock_guard<std::mutex> guard(account_mutex);
if (amount <= 0) {
return; // деструктор guard разблокирует мьютекс
}
do_transfer(amount); // исключение раскрутит стек и вызовет деструктор
}
Во втором варианте нет ни одной строки, посвящённой освобождению, и при этом ни один путь выхода не оставит мьютекс заблокированным. Именно это отличает C++ от Go или Java с их defer/try-finally: там освобождение остаётся действием в теле функции, а не свойством объекта, и его всё ещё можно забыть написать.
Что такое раскрутка стека
Раскрутка стека (stack unwinding) — процесс, который запускается при выбросе исключения: среда выполнения уничтожает все автоматические объекты в текущей области видимости в порядке, обратном созданию, пока не найдёт подходящий обработчик catch. Именно на этом механизме держится вся надёжность RAII.
Стандартные RAII-обёртки, которые уже написаны за вас
Своя обёртка нужна редко — стандартная библиотека покрывает большинство ресурсов. Ниже — что чем закрывать в современном C++ (C++17 и новее).
| Ресурс | RAII-обёртка | Когда использовать |
|---|---|---|
| Динамическая память, единоличное владение | std::unique_ptr | По умолчанию для любого владеющего указателя |
| Память с разделяемым владением | std::shared_ptr | Когда владельцев объективно несколько |
| Мьютекс, простая блокировка | std::lock_guard | Захват на всю область видимости без манёвров |
| Мьютекс с гибким управлением | std::unique_lock | Отложенный захват, condition_variable, ручной unlock |
| Файл | std::fstream | Закрывается в деструкторе автоматически |
| Массив с владением | std::vector | Вместо new[]/delete[] почти всегда |
Отдельно: std::make_unique (доступен с C++14) и std::make_shared создают объект и обёртку одним вызовом, без голого new в вашем коде. Правило простое: если в новом коде встречается new вне тела конструктора RAII-класса — это повод для замечания на ревью.
Когда приходится писать свою обёртку
C-библиотеки отдают ресурсы через пары функций вроде fopen/fclose или собственные хендлы. Обернуть такой хендл можно двумя способами.
Через кастомный deleter у unique_ptr
Самый дешёвый вариант, когда ресурс — это указатель или его можно к нему свести:
#include <cstdio>
#include <memory>
// Тип deleter'а, вызывающий fclose
struct FileCloser {
void operator()(std::FILE* file) const {
if (file) {
std::fclose(file);
}
}
};
using FilePtr = std::unique_ptr<std::FILE, FileCloser>;
FilePtr open_config(const char* path) {
// fopen вернёт nullptr при ошибке — проверку делает вызывающий код
return FilePtr(std::fopen(path, "r"));
}
Отдельным классом с правилом пяти
Если ресурс сложнее указателя или нужна дополнительная логика, пишут класс. Здесь всплывает правило пяти: как только вы объявили деструктор, придётся осознанно решить судьбу конструктора копирования, копирующего присваивания, перемещающего конструктора и перемещающего присваивания. Для владеющего ресурсом класса копирование обычно запрещают, а перемещение реализуют — иначе получите двойное освобождение.
#include <utility>
class DbConnection {
public:
explicit DbConnection(const char* dsn)
: handle_(db_connect(dsn)) {} // захват в конструкторе
~DbConnection() { // освобождение в деструкторе
if (handle_) {
db_close(handle_);
}
}
// Копирование запрещаем: два владельца одного хендла недопустимы
DbConnection(const DbConnection&) = delete;
DbConnection& operator=(const DbConnection&) = delete;
// Перемещение: передаём владение, у источника обнуляем хендл
DbConnection(DbConnection&& other) noexcept
: handle_(std::exchange(other.handle_, nullptr)) {}
DbConnection& operator=(DbConnection&& other) noexcept {
if (this != &other) {
if (handle_) {
db_close(handle_);
}
handle_ = std::exchange(other.handle_, nullptr);
}
return *this;
}
private:
DbHandle* handle_;
};
Если бы вы забыли запретить копирование, компилятор сгенерировал бы почленное копирование, два объекта указывали бы на один handle_, и оба вызвали бы db_close — тот самый double free.
Где RAII ломается или требует осторожности
Принцип надёжен, но у него есть граничные случаи, о которых стоит помнить.
- Исключение из деструктора. Если деструктор бросает исключение во время раскрутки стека, вызывается
std::terminate. Поэтому деструкторы по умолчаниюnoexcept, а освобождение ресурса не должно бросать. Транзакцию, которая может завершиться ошибкой, откатывайте явным методом, а деструктор оставляйте для аварийного отката без исключений. - Циклические ссылки shared_ptr. Два объекта, держащие
shared_ptrдруг на друга, никогда не обнулят счётчик — это утечка, которую RAII сам не разорвёт. Разрывают её черезstd::weak_ptr. - Порядок разрушения между единицами трансляции. Для глобальных и статических объектов порядок уничтожения между разными
.cpp-файлами не определён. Не полагайтесь на то, что один глобальный RAII-объект переживёт другой. - Ресурс, освобождаемый не тем потоком. Некоторые API требуют освобождать хендл в том же потоке, что и захватил. RAII в этом случае может уничтожить объект не там, где нужно — следите за временем жизни явно.
Что делать дальше
Пройдите по своему коду и найдите каждый голый new, delete, lock/unlock и fopen/fclose. Каждая такая пара — кандидат на замену RAII-обёрткой. Порядок действий: сначала переведите владение памятью на std::unique_ptr и std::vector, затем блокировки на std::lock_guard, и только оставшиеся C-хендлы оборачивайте вручную. Для проверки правильности перемещения и отсутствия двойного освобождения запустите сборку под AddressSanitizer (флаг -fsanitize=address в GCC и Clang) — он ловит use-after-free и double free на этапе тестов, а не в проде.
Репетитор 1—11 класс и ВУЗ: Математика и физика методом наведения на мысль, помощь с ДЗ.
Спросить за 8 ₽Источники: RAII — cppreference.com, C++ Core Guidelines: Resource management
Частые вопросы
Чем RAII отличается от сборщика мусора?
У меня старый компилятор без C++14, make_unique недоступен. Что делать?
Обязательно ли запрещать копирование в своём RAII-классе?
Что будет, если деструктор RAII-обёртки бросит исключение?
RAII полностью защищает от утечек памяти?
Стоит ли писать свою обёртку или искать готовую?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.