Что такое RAII простыми словами
RAII — Resource Acquisition Is Initialization, «захват ресурса есть инициализация». Идея: любой ресурс (память, файл, сокет, мьютекс, соединение с БД) оборачивается в объект. Конструктор объекта ресурс захватывает, деструктор — гарантированно освобождает, когда объект выходит из области видимости. Неважно, как происходит выход: нормальный возврат, break, return посреди функции или раскрутка стека при исключении — деструктор будет вызван всегда.
void process() {
std::lock_guard lock(mutex); // мьютекс захвачен
// ... работаем ...
// при выходе из функции — любому — мьютекс отпустится сам
}
До RAII тот же код выглядел бы так: захватили мьютекс, начали работать, получили исключение — мьютекс так и остался захваченным, дедлок навсегда. C++ превращает корректность в «бесплатную» — она следует из правил языка, а не из внимательности программиста.
Куда RAII уже встроен в стандартную библиотеку
Вы пользуетесь RAII постоянно, может, не подозревая об этом:
- Память:
std::string,std::vector,std::unique_ptr,std::shared_ptr— освобождают кучу сами. - Файлы:
std::ifstream/std::ofstreamзакрывают дескриптор в деструкторе. - Блокировки:
std::lock_guard,std::unique_lock,std::scoped_lock(C++17, для нескольких мьютексов сразу). - Потоки:
std::jthread(C++20) делает join в деструкторе — поток не останется бесхозным. - Счётчики времени, скоуп-гварды: свои классы или
gsl::final_action.
Пишем свою RAII-обёртку: файл в C-стиле
Старые C-API работают через сырые указатели и функции fclose, close, free. Оборачиваем их, чтобы нельзя было забыть освобождение:
class File {
FILE* f_;
public:
explicit File(const char* path, const char* mode) : f_(fopen(path, mode)) {
if (!f_) throw std::runtime_error("не открыть файл");
}
~File() { if (f_) fclose(f_); }
// запрещаем копирование — владелец один
File(const File&) = delete;
File& operator=(const File&) = delete;
// перемещение разрешаем
File(File&& o) noexcept : f_(o.f_) { o.f_ = nullptr; }
FILE* get() const { return f_; }
};
Три обязательных правила RAII-класса:
- Конструктор захватывает ресурс или кидает исключение.
- Деструктор освобождает, никогда не кидает исключений (
noexceptпо умолчанию). - Копирование запрещено или осмыслено: для единоличного владения —
delete, для разделяемого — счётчик ссылок.
Пример: измерение времени через RAII
Класс, который в конструкторе запоминает время, а в деструкторе печатает длительность — scope-таймер:
class Timer {
std::string name_;
std::chrono::steady_clock::time_point t0_ = std::chrono::steady_clock::now();
public:
explicit Timer(std::string name) : name_(std::move(name)) {}
~Timer() {
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - t0_).count();
std::cout << name_ << ": " << ms << " ms\n";
}
};
void heavy() {
Timer t{"heavy"};
// ... работа ...
} // "heavy: 153 ms"
RAII и исключения: почему это работает
Магия — в гарантии языка: если объект полностью сконструирован на стеке, его деструктор вызовется и при раскрутке стека. Поэтому стандартный совет «ловите по const&, кидайте по значению, а ресурсы держите в RAII» делает исключения безопасными:
void risky() {
auto data = std::make_unique<Buffer>(1024);
mayThrow(); // исключение? не страшно — data удалится при раскрутке
alsoMayThrow();
}
Деструктор, который сам бросает исключение, — антипаттерн: если он сработает во время раскрутки другого исключения, получите std::terminate. Деструкторы должны молча деградировать (поглощать ошибки или логировать).
Scoped guard для произвольных действий
Когда нет готовой обёртки, пишут универсальный scope-guard — объект с лямбдой, которая исполняется в деструкторе:
template<class F>
struct ScopeExit {
F f;
~ScopeExit() { f(); }
};
void setup() {
turnOnPower();
ScopeExit off{[]{ turnOffPower(); }}; // выключится при любом выходе
// ...
}
В С++26 ожидается std::scope_exit стандартно; до того — такие классы или библиотеки (GSL final_action).
RAII во встраиваемых системах
Для Arduino/микроконтроллеров RAII особенно ценен: там нет ОС, которая «приберётся» за утечкой. Обёртка над CS-пином SPI, над Wire.begin/end, над захватом прерываний — всё это RAII. Исключений на AVR обычно нет, но RAII работает и без них — деструктор вызывается при выходе из scope всё равно.
class SpiSelect {
uint8_t pin_;
public:
explicit SpiSelect(uint8_t pin) : pin_(pin) { digitalWrite(pin_, LOW); }
~SpiSelect() { digitalWrite(pin_, HIGH); }
};
void readSensor() {
SpiSelect cs(SS_PIN);
// SPI.transfer(...) — пин отпустится сам
}
Итог
RAII — это не фича, а способ мысли: каждый ресурс имеет владельца-объект, время жизни ресурса равно времени жизни объекта, деструкторы не бросают. Усвоив это, вы пишете код, который корректен при любых путях выхода — и перестаёте бояться исключений и ранних return.
RAII в стандартной библиотеке: полный список
Зная RAII, вы узнаёте его везде:
std::unique_ptr,shared_ptr,weak_ptr— память;std::lock_guard,unique_lock,scoped_lock,shared_lock— блокировки;std::fstream,ifstream,ofstream— файлы;std::string,vector,map— динамические буферы;std::jthread— поток с автоджойном;std::experimental::scope_exit/ сторонниеgsl::final_action— произвольный cleanup.
Правило нуля: лучший класс — без деструктора
Если все ресурсы уже в RAII-обёртках, ваш класс не пишет деструктор вовсе:
class App {
std::vector<Job> jobs_;
std::unique_ptr<Connection> conn_;
std::ofstream log_;
// деструктор не нужен — всё умирает само
};
«Правило нуля»: класс, чьи члены сами управляют ресурсами, не требует ни деструктора, ни конструкторов копирования — компилятор сгенерирует корректные.
RAII и возврат ошибок
Иногда исключение недопустимо (real-time, embedded без исключений). RAII всё равно работает: деструктор вызывается при выходе из scope независимо от пути. Ресурс закрывается и при return раньше времени, и при break из цикла — код безопасен даже без механизма исключений.
Типичная реализация scope-guard из boost/GSL
#include <gsl/util>
void transaction() {
beginWork();
auto commit = gsl::finally([] { if (!committed) rollback(); });
// работаем...
committed = true; // в конце — флаг отменяет rollback
}
Деструктор finally вызовется всегда — и откатит только если мы не успели закоммитить.
Итоговое правило
Перестаньте думать «где тут delete». Думайте «у какого объекта это время жизни». Ответ почти всегда — RAII-обёртка или контейнер. Если вы видите в коде delete, fclose, unlock, free — это место, где должен быть RAII-класс.