Заметки по языку с и с++

RAII в C++: как избежать утечек ресурсов без сборщика мусора

Введение

C++ не имеет сборщика мусора, и многие новички видят в этом источник опасности: «как же я не забуду освободить память или закрыть файл?». Ответ языка на этот вопрос — идиома RAII (Resource Acquisition Is Initialization, «получение ресурса есть инициализация»). Это не библиотека и не ключевое слово, а способ мышления: ресурс захватывается в конструкторе объекта и освобождается в его деструкторе. Поскольку деструкторы локальных объектов в C++ вызываются гарантированно — при выходе из блока, при возврате из функции и даже при исключении — ресурс не может «потеряться».

Суть концепции

Любой ресурс: память, файловый дескриптор, мьютекс, сетевое соединение, объект Qt с явным владением — можно «обернуть» в класс, который захватывает ресурс в конструкторе и освобождает в деструкторе. Дальше вы просто работаете с объектом как с обычной переменной на стеке, а компилятор и правила времени жизни C++ делают всё остальное.

#include <iostream>
#include <stdexcept>
#include <fstream>

class FileGuard {
public:
    explicit FileGuard(const std::string& path)
        : file_(path)
    {
        if (!file_.is_open()) {
            throw std::runtime_error("Не удалось открыть файл: " + path);
        }
    }

    // Запрещаем копирование: ресурс уникален
    FileGuard(const FileGuard&) = delete;
    FileGuard& operator=(const FileGuard&) = delete;

    std::ofstream& stream() { return file_; }

    ~FileGuard()
    {
        // Деструктор вызывается всегда, даже при исключении выше по стеку
        if (file_.is_open()) {
            file_.close();
        }
    }

private:
    std::ofstream file_;
};

void writeLog(const std::string& message)
{
    FileGuard guard("log.txt");
    guard.stream() << message << std::endl;

    if (message.empty()) {
        throw std::invalid_argument("Пустое сообщение");
        // Файл всё равно будет закрыт — деструктор FileGuard вызовется
        // при раскрутке стека (stack unwinding).
    }
}

int main()
{
    try {
        writeLog("Запуск приложения");
        writeLog("");
    } catch (const std::exception& e) {
        std::cerr << "Ошибка: " << e.what() << std::endl;
    }
    return 0;
}

Разбор примера

Класс FileGuard захватывает файл в конструкторе и гарантированно закрывает его в деструкторе. Когда внутри writeLog бросается исключение, стек начинает «раскручиваться» (stack unwinding): C++ вызывает деструкторы всех локальных объектов в порядке, обратном их созданию, прежде чем передать исключение наверх. Именно поэтому guard корректно закроет файл, даже если мы никогда явно не вызывали close() в точке выброса исключения.

Обратите внимание на запрет копирования: если у ресурса единственный владелец, копирование объекта-обёртки привело бы к двойному освобождению ресурса. Для случаев, когда владение нужно передавать, используют семантику перемещения (move semantics) — это естественное продолжение RAII, легшее в основу std::unique_ptr, std::vector, std::lock_guard и большинства стандартных классов.

Подводные камни и советы

  • Деструктор не должен бросать исключения. Если в деструкторе возможна ошибка (например, close() файла может завершиться неудачно), её нужно либо проглотить, либо залогировать, но не пробрасывать дальше — иначе при одновременном раскручивании стека программа завершится через std::terminate.
  • Не используйте RAII-объекты через new/delete вручную — это убивает весь смысл идиомы. Размещайте их на стеке или внутри других объектов с автоматическим временем жизни.
  • Используйте готовые обёртки: std::lock_guard, std::unique_lock для мьютексов, std::unique_ptr/std::shared_ptr для памяти, QScopedPointer в Qt-коде. Писать свой RAII-класс стоит только для нестандартных ресурсов (сокеты, дескрипторы ОС, сторонние C API).
  • Порядок освобождения — обратный порядку создания. Если ресурс B зависит от ресурса A, создавайте A раньше B, чтобы B освободился первым.
  • В Qt не забывайте, что QObject с родителем управляется деревом владения Qt, а не RAII в чистом виде — это отдельная модель, которую не стоит путать со стековым RAII.

RAII — это не «фича для экспертов», а базовый стиль написания надёжного C++ кода. Освоив его, вы автоматически избавитесь от большинства утечек ресурсов и забытых delete/close/unlock.

================================================================================