специфика C++ — управление ресурсами без сборщика мусораC++-specific — resource management without a garbage collector
Resource Acquisition Is Initialization — не паттерн проектирования, а прямое следствие гарантии языка: деструктор объекта со стековым временем жизни вызывается детерминированно и всегда, включая путь через исключение. В C++ не пишут код очистки — пишут деструкторы.Resource Acquisition Is Initialization isn't a design pattern — it's a direct consequence of a language guarantee: a stack-lifetime object's destructor runs deterministically and always, including along an exception path. In C++ you don't write cleanup code — you write destructors.
RAII — идиома, при которой владение ресурсом (памятью, файловым дескриптором, мьютексом, сетевым соединением) привязывается к времени жизни объекта: ресурс захватывается в конструкторе и освобождается в деструкторе. Поскольку деструктор стекового объекта вызывается компилятором автоматически и гарантированно при выходе из области видимости — обычном или через исключение — ресурс не может "случайно" остаться неосвобождённым, если только сам деструктор не написан некорректно.RAII is the idiom where a resource's ownership (memory, a file handle, a mutex, a network connection) is tied to an object's lifetime: the resource is acquired in the constructor and released in the destructor. Because a stack object's destructor is called automatically and guaranteed by the compiler on scope exit — whether normal or via an exception — the resource can't "accidentally" stay unreleased, unless the destructor itself is written incorrectly.
class FileHandle {
FILE* f;
public:
explicit FileHandle(const char* path) : f(fopen(path, "r")) {} // захват в конструкторе// acquisition in the constructor
~FileHandle() { if (f) fclose(f); } // освобождение в деструкторе// release in the destructor
};
void process() {
FileHandle file("data.txt");
doWork(); // если doWork() бросит исключение — file всё равно корректно закроется// if doWork() throws — file still closes correctly
} // или здесь, при обычном выходе из функции// or here, on normal exit from the function
В языках без RAII (C, ранняя практика Java до try-with-resources) корректная очистка при исключении требует ручного try/finally вокруг каждого захваченного ресурса — при вложенных ресурсах это быстро превращается в пирамиду вложенных блоков, и достаточно забыть один finally, чтобы получить утечку, срабатывающую только на редком пути с исключением (и потому плохо тестируемую).In languages without RAII (C, early Java practice before try-with-resources), correct cleanup on exception requires a manual try/finally around every acquired resource — with nested resources this quickly turns into a pyramid of nested blocks, and forgetting just one finally produces a leak that only fires on the rare exception path (and is therefore poorly tested).
В C++ размотка стека (stack unwinding) при выбросе исключения вызывает деструкторы ВСЕХ полностью сконструированных локальных объектов в обратном порядке их создания — автоматически, без единой явной инструкции очистки в теле функции. Это переносит ответственность за корректность из каждой точки использования ресурса в одно место — код деструктора самого ресурсного типа, написанный и проверенный один раз.In C++, stack unwinding on exception invokes the destructors of ALL fully-constructed local objects, in reverse order of construction — automatically, with not a single explicit cleanup instruction in the function body. This moves the correctness burden out of every point of use of a resource and into one place — the resource type's own destructor code, written and verified once.
void transfer(Account& from, Account& to, int amount) {
std::lock_guard<std::mutex> lockFrom(from.mutex); // захват мьютекса — RAII// mutex acquisition — RAII
std::lock_guard<std::mutex> lockTo(to.mutex);
if (from.balance < amount) throw std::runtime_error("insufficient funds");
// оба мьютекса корректно разблокируются здесь при throw — ни единой ручной строки для этого не написано// both mutexes correctly unlock here on throw — not a single manual line written for it
from.balance -= amount;
to.balance += amount;
}
Значительная часть стандартной библиотеки — RAII-обёртки над ресурсами, которыми в C пришлось бы управлять вручную: std::unique_ptr/std::shared_ptr (память), std::lock_guard/std::unique_lock (мьютекс), std::fstream (файловый дескриптор), std::jthread (поток — в отличие от std::thread, автоматически join-ится в деструкторе), std::vector (динамический массив).A large part of the standard library is RAII wrappers over resources that would need manual management in C: std::unique_ptr/std::shared_ptr (memory), std::lock_guard/std::unique_lock (a mutex), std::fstream (a file handle), std::jthread (a thread — unlike std::thread, automatically joined in its destructor), std::vector (a dynamic array).
Интервью-формулировка этой идеи: "In C++ I don't write cleanup code — I write destructors" — практический результат в том, что функции, работающие с ресурсами, обычно не содержат явного кода освобождения вообще, только объявления RAII-объектов в нужных местах.The interview phrasing of this idea: "In C++ I don't write cleanup code — I write destructors" — the practical result is that functions working with resources usually contain no explicit release code at all, just declarations of RAII objects in the right places.
Создание RAII-объекта через new без немедленной передачи его во владеющий умный указатель (new FileHandle(...) как есть, без unique_ptr) полностью убивает гарантию — деструктор больше не вызывается автоматически, и вся идея RAII превращается обратно в ручное управление через delete.Creating an RAII object via new without immediately handing it to an owning smart pointer (new FileHandle(...) as-is, with no unique_ptr) completely kills the guarantee — the destructor is no longer called automatically, and the whole point of RAII collapses back into manual management via delete.
Если деструктор RAII-объекта сам бросает исключение в момент, когда стек уже разматывается из-за ДРУГОГО исключения, программа немедленно завершает работу через std::terminate — ровно поэтому деструкторы по соглашению обязаны быть noexcept (и являются таковыми неявно, если не указано обратное).If an RAII object's own destructor throws an exception while the stack is already unwinding because of ANOTHER exception, the program immediately terminates via std::terminate — this is exactly why destructors are conventionally required to be noexcept (and are implicitly so unless stated otherwise).
Если два ресурса зависят друг от друга (например, соединение должно закрыться раньше логгера, который пишет диагностику при закрытии), порядок ОБЪЯВЛЕНИЯ RAII-объектов в теле функции определяет порядок их разрушения — объявленный позже разрушается раньше. Перепутанный порядок объявления — источник тонких багов "работает, пока не работает".If two resources depend on each other (say, a connection must close before the logger that writes diagnostics on close), the DECLARATION order of the RAII objects in the function body determines their destruction order — the one declared later is destroyed first. Mixed-up declaration order is a source of subtle "works until it doesn't" bugs.