Привет, Хаброжители!

1. Введение

В современном C++ управление ресурсами — это ключевая составляющая корректности программы, затрагивающая память, дескрипторы файлов, блокировки и все внешние системы, с которыми приходится взаимодействовать вашему коду. Начинающие программисты часто полагают, что при работе с C++ требуется активно очищать память вручную, пользуясь командами new, delete, malloc или free. Но на самом деле в современном C++ эта работа строится существенно иначе. Программисту сложно запомнить, когда и как высвобождать ресурсы, поэтому в языке теперь предлагается принцип проектирования RAII, означающий, что «Приобретение ресурса равноценно его инициализации».

RAII связывает время жизни ресурса со временем жизни объекта. Когда объект создаётся, он приобретает ресурс прямо в рамках своего конструктора. Когда объект выходит за пределы области видимости, его деструктор автоматически освобождает этот ресурс. Этот принцип универсален и применим к памяти, файлам, сетевым соединениям, мьютексам и многому другому. Работая таким образом с областью видимости объекта, C++ гарантирует надёжное выполнение очистки, даже в случае преждевременного выхода из функции, или если будет выброшено исключение.

В частности, именно благодаря такой детерминированной очистке C++ остаётся высокопроизводительным системным языком, для работы с которым не нужен сборщик мусора. Принцип RAII превращает правильную очистку в языковую фичу, а не заставляет программиста отвечать за такую очистку. В результате получается более безопасный и предсказуемый код.

2. В чём смысл RAII

RAII — это простая идея с далеко идущими последствиями. Ресурс приобретается при создании объекта и автоматически высвобождается по окончании времени жизни объекта. Поэтому программисту не приходится вручную управлять такими функциями как open (открыть), close (закрыть), lock (заблокировать), unlock (разблокировать), new (создать) и (удалить). Вместо этого сам язык гарантирует, что очистка произойдёт сразу же после выхода объекта из области видимости — тогда сработает деструктор этого объекта. Таким образом удаётся более предсказуемо управлять ресурсами и избегать распространённых ошибок — например, не забывать высвободить какой-то ресурс или не высвобождать его в неподходящий момент.

На практике RAII стимулирует разработчиков задумываться о владении. Если объект владеет ресурсом, то этот ресурс должен обрабатываться в пределах конструктора и деструктора этого объекта. Так гарантируется, что, независимо от того, как пойдёт выполнение программы, произойдет ли обычный возврат, или будет выброшено исключение, деструктор обязательно выполнится, а значит,  очистка произойдёт как следует.

Рис. 1. Развитие времени жизни в ходе RAII
Рис. 1. Развитие времени жизни в ходе RAII

3. Базовый паттерн RAII

Механизм RAII работает, так как сам ресурс и его время жизни привязаны непосредственно к объекту в стеке. Когда объект создаётся, конструктор приобретает ресурс. Когда объект покидает область видимости, деструктор автоматически его высвобождает. Поэтому вам не приходится вручную вызывать функции очистки, и никогда не нужен блок finally. Всё это обрабатывает сам язык.

class FileHandle {
public:
    FileHandle(const std::string& path) {
        file = std::fopen(path.c_str(), "r");
        if (!file) throw std::runtime_error("Failed to open file.");
    }
    
    ~FileHandle() {
        /* Автоматически высвобождает ресурс, когда объект выходит из области видимости */
        std::fclose(file); 
    }
private:
    FILE* file;
};

void example() {
    FileHandle fh("data.txt"); // Здесь файл открывается
    // Чтение файла...
} // Здесь файл автоматически закрывается

int main() {}

Этот пример демонстрирует сущность RAII. Не бывает так, чтобы функция явно закрывала файл. Деструктор гарантирует очистку. Даже если функция возвращается преждевременно или выбрасывает исключение, FileHandle позаботится о том, чтобы корректно закрыть ресурс.

4. Реалистичный пример RAII в стандартной библиотеке

RAII — не просто подход к проектированию, но и движущая основа многих важных возможностей, предусмотренных в стандартной библиотеке C++. Во всех случаях, когда нужно обеспечить правильное приобретение и высвобождение ресурса, именно RAII гарантирует очистку, даже, если что-то пойдёт не так.

Один из лучших примеров такого рода — безопасная блокировка в многопоточном коде. Можно не вызывать lock() и unlock() вручную, так как в C++ для этой цели предлагаются типы наподобие std::lock_guard и std::unique_lock. Эти классы принимают блокировку у себя в конструкторе и высвобождают её у себя в деструкторе. Программисту не приходится беспокоиться о том, снял ли он мьютекс. Очистка происходит автоматически, как только объект покинет область видимости.

#include <mutex>
#include <queue>
std::mutex qMutex;
std::queue<int> q;
void push_value(int v)
{
  std::lock_guard<std::mutex> lock(qMutex); /* здесь приобретается блокировка */
  q.push(v); /* безопасный доступ к совместно используемой очереди */
} /* блокировка автоматически снимается */

Тот же паттерн применяется при обработке файлов и управлении памятью. Конструктор приобретает ресурс, а деструктор высвобождает его, когда объект выходит за пределы области видимости.

#include <fstream>
#include <memory>

void save_message(const std::string& msg)
{
  std::ofstream file("log.txt"); /*файл открыт */
  file << msg; /* здесь происходит запись */
} /* файл автоматически закрывается */

void allocate_value()
{
  std::unique_ptr<int> ptr = std::make_unique<int>(42); /* память выделена */
} /* память автоматически высвобождена */

При помощи RAII в коде можно выразить намерение, а не заниматься учётом. Вы сосредотачиваетесь на том, что должна делать программа, а язык гарантирует, что все ресурсы приобретаются и высвобождаются правильно. Благодаря этому удаётся писать более простой код и допускать меньше багов, особенно в сложных системах.

5. Заключение

Таким образом, RAII автоматизирует управление ресурсами в C++.
Когда ресурс привязан к объекту, очистка выполняется автоматически, поэтому его не приходится вручную высвобождать, разблокировать или закрывать. Это позволяет избегать таких распространённых багов как утечки памяти, двойное высвобождение или забытый ресурс, который нужно было разблокировать.

Файлы закрываются сами, память высвобождается по умным указателям, мьютексы автоматически снимаются.  RAII реализует управление временем жизни как языковую возможность, снимая с программиста ответственность за это.

Если работать с RAII в уме, то вы заботитесь только о владении ресурсом, а C++ отвечает за его высвобождение.