Паттерн RAII

Тема дорожной карты · Modern C++ Developer

RAII (Resource Acquisition Is Initialization) — центральная идиома C++, без которой невозможно писать надёжный код на этом языке. Суть проста: время жизни ресурса привязывается к времени жизни объекта на стеке. Конструктор захватывает ресурс, деструктор его освобождает, а компилятор гарантирует вызов деструктора при любом выходе из области видимости — включая исключения и ранние return. Именно RAII делает C++ языком без сборщика мусора, но с детерминированным управлением памятью, файлами, сокетами и блокировками.

Как это работает

Каждый объект с автоматическим временем хранения уничтожается в момент выхода из блока, в котором он объявлен, причём в порядке, обратном созданию. Если конструктор класса открывает файл, захватывает мьютекс или выделяет память, то деструктор обязан выполнить обратную операцию. Программисту не нужно помнить про освобождение: компилятор вставляет вызовы деструкторов на всех путях выполнения, включая раскрутку стека при выброшенном исключении.

Классические примеры из стандартной библиотеки — std::unique_ptr, std::lock_guard, std::fstream, std::vector. Все они владеют чем-то (памятью, блокировкой, дескриптором файла, буфером) и гарантируют корректное освобождение. Собственный RAII-класс пишется по той же схеме: захват в конструкторе, освобождение в деструкторе, а копирование либо запрещается (= delete), либо заменяется перемещением, чтобы у ресурса всегда был ровно один владелец.

class FileHandle {
  FILE* f_;
public:
  explicit FileHandle(const char* path) : f_(std::fopen(path, "rb")) {
    if (!f_) throw std::runtime_error("open failed");
  }
  ~FileHandle() { std::fclose(f_); }
  FileHandle(const FileHandle&) = delete;
  FileHandle& operator=(const FileHandle&) = delete;
};

Важное следствие — правило «нуля» и «пяти». Если класс владеет ресурсом через RAII-обёртки (умные указатели, контейнеры), ему не нужны собственные деструктор и операции копирования. Если же класс сам управляет сырым ресурсом, придётся аккуратно определить все пять специальных функций.

Когда применять

RAII применяется всегда, когда есть парная операция «получить — вернуть»: память и delete, fopen и fclose, lock и unlock, socket и close, транзакция и commit/rollback, SetCursor и восстановление прежнего курсора. Особенно ценна идиома в коде с исключениями: без RAII каждая функция превращается в лестницу try/catch, где легко пропустить один из путей. Оборачивайте в RAII любые ресурсы сторонних C-библиотек — OpenSSL, libcurl, SQLite, Win32 API. Для одноразового кода удобны обёртки вроде std::unique_ptr с пользовательским делитером или gsl::finally, чтобы не заводить отдельный класс.

Типичные ошибки

Самая частая проблема — исключение, выброшенное из деструктора. Во время раскрутки стека это приводит к std::terminate, поэтому деструкторы должны быть noexcept и обрабатывать ошибки освобождения молча или через логирование. Вторая ошибка — забытые операции копирования: компилятор сгенерирует их по умолчанию, и два объекта дважды освободят один дескриптор. Третья — временный объект вместо именованной переменной: строка std::lock_guard<std::mutex>(m); объявляет переменную с именем m, а не захватывает мьютекс, и блокировка не происходит. Наконец, RAII не спасает от циклических ссылок std::shared_ptr — здесь нужен std::weak_ptr или пересмотр модели владения.

Связанные понятия

Полезные ресурсы

🔤 Английские термины в этой теме

Слова из этой темы, которые русскоязычные часто произносят неверно. Кнопка 🔊 произносит голосом браузера.

Весь словарь терминов →