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

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++ отвечает за его высвобождение.

 

Комментарии (38)


  1. eoanermine
    19.12.2025 09:31

    Хаб „Параллельное программирование“ для переведенной ИИ-сгенерированной новичковой статьи про RAII. Огонь.

    И даже форматирование в листинге с FileHandle не потрудились поправить.


    1. domix32
      19.12.2025 09:31

      А уж как замечательно кидать исключения в конструкторе


      1. deelayka
        19.12.2025 09:31

        В конструкторе можно выбрасывать исключения, в отличие от деструктора.


        1. voldemar_d
          19.12.2025 09:31

          Можно вообще много что делать, но некоторых вещей лучше избегать.


          1. naviUivan
            19.12.2025 09:31

            Единственный способ сообщить об ошибке в конструкторе, так это бросить исключение. Тоесть это правильно, если в этом есть необходимость, а не какие-то религиозные - лучше не делать. Почему лучше?


            1. voldemar_d
              19.12.2025 09:31

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


              1. coodi
                19.12.2025 09:31

                А ещё они на php очень долго работали?


                1. voldemar_d
                  19.12.2025 09:31

                  Не знаю, о чем Вы. Я про c++


              1. eao197
                19.12.2025 09:31

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

                ЕМНИП, правило не бросать исключения из конструктора в свое время насаждалось не потому, что были некие непривыкшие ловить исключения программисты. А потому, что еще до появления широкой поддержки исключений в большинстве C++ных компиляторов (а это произошло, увы далеко не сразу) появилось множество библиотек, которые не были расчитаны на применение исключений вообще. Навскидку вспоминаются Qt и MFC, которые использовались ну очень широко.

                И вот в таких библиотеках применять исключения было чревато. В том числе и в конструкторах.

                Т.е. основная причина не в том, что выброс исключения из конструктора для кого-то из программистов может быть неожиданностью. А в том, что применение исключений (в том числе и из конструкторов) было чревато при интеграции со старыми библиотеками, которые создавались еще без оглядки на наличие исключений в языке.

                AFAIK, запрет на исключения в печально знаменитом Google Code Guide имеет те же самые корни.


                1. voldemar_d
                  19.12.2025 09:31

                  Видимо, причин много, и разных. А Qt и сейчас ещё как используется, даже если и считается старой.


                  1. eao197
                    19.12.2025 09:31

                    Просто сейчас, если исключения в проекте разрешены, то глупо отказываться бросать их из конструктора. Единственная причина поставить исключения из конструктора вне закона -- это как раз таки интеграция с кодом, который на исключения не расчитан вообще.

                    А апелляции к тому, что так не было принято в Facebook-е или Google без уточнения конкретных причин -- ну это просто пиетет перед громкими именами и ничего более.


                    1. voldemar_d
                      19.12.2025 09:31

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

                      Стиль написания кода при использовании исключений все же серьезно отличается, и это влияет и на читабельность кода.


                      1. eao197
                        19.12.2025 09:31

                        Стиль написания кода при использовании исключений все же серьезно отличается,

                        Чем же и почему?

                        и это влияет и на читабельность кода.

                        И что, на ваш взгляд, читается лучше -- с исключениями или без?


                      1. voldemar_d
                        19.12.2025 09:31

                        Лично для меня код без исключений более нагляден.

                        Одно дело - явно видеть, как и где проверяются возвращаемые из вызовов коды ошибок, а другое - сплошной код без проверок, но нужно все время помнить, что в любой строке может быть выкинуто исключение, которое может быть отловлено вообще не здесь, а в вызывающей процедуре. И что даже создание экземпляра класса может выкинуть исключение.

                        Возможно, это дело привычки и опыта.


                      1. eao197
                        19.12.2025 09:31

                        все время помнить, что в любой строке может быть выкинуто исключение, которое может быть отловлено вообще не здесь, а в вызывающей процедуре

                        А зачем об этом помнить и думать?


                      1. voldemar_d
                        19.12.2025 09:31

                        Можно ни о чем не думать. Особенно когда программа выполняет недопустимую операцию на машине у клиента, а ты потом пытаешься понять, почему.


                      1. eao197
                        19.12.2025 09:31

                        Можно ни о чем не думать.

                        А если не сваливаться в морализаторство и попробовать обсудить чем программирование в присутствии исключений отличается от программирования в отсутствии оных?

                        Двайте сейчас на брать в рассмотрение специальных noexcept-контекстов (вроде кода деструкторов, секций catch и noexcept-методов). Просто обычный код.

                        Что такое привносят исключения, что требует переосмысления при написании кода?


                      1. voldemar_d
                        19.12.2025 09:31

                        Знаете, я не очень готов это как-то подробно обсуждать. На эту тему достаточно много материалов готовых можно найти. Я уже написал, что сам привык работать без обработки исключений. И мне не нравится, когда вызываешь чужой код, а он роняет мою программу в непредсказуемый момент при обработке каких-то входных данных (и не всегда очевидно, каких именно). Окружать вызовы чужого кода блоками try/catch/except мне тоже не нравится. Реального преимущества использования исключений в моей практике я не видел - ни с точки зрения быстродействия, ни удобства написания кода. Свою точку зрения я никому не навязываю, тем более, что она основана, скорее, на "нравится/не нравится". Но раз есть другие люди, которым также неудобно или не хочется работать с обработкой исключений, я считаю, что это не просто какая-то блажь.

                        P.S. И уж тем более я не собирался заниматься морализаторством.


                      1. eao197
                        19.12.2025 09:31

                        Я уже написал, что сам привык работать без обработки исключений. И мне не нравится, когда вызываешь чужой код, а он роняет мою программу в непредсказуемый момент при обработке каких-то входных данных (и не всегда очевидно, каких именно).

                        Правильно ли я понимаю, что вы запрещаете исключения на уровне ключей компилятора?


                      1. voldemar_d
                        19.12.2025 09:31

                        Нет, такого я не делаю.

                        Изредка бывает, что приходится использовать чужие библиотеки или готовые классы, которые кидают исключения, ради них приходится использовать их "отлов".


      1. artalex
        19.12.2025 09:31

        А как Вы предлагаете сообщать об ошибке из конструктора?


        1. domix32
          19.12.2025 09:31

          Совершенно точно разделять логику инициализации и проверки корректности.

          Вариант 1. Приватный конструктор и статичный метод

          static FileHandle FileHandle::create(fs::path path) {
            auto raw_handle = fs::open(path);
            if (!raw_handle) throw InvalidFilePathException{};
            return FileHandle(raw_andle);
          }
          // ну или на опционалах
          
          // для с++17
          static std::optional<FileHandle> FileHandle::create(fs::path path) {
            auto raw_handle = fs::open(path);
            if (!raw_handle) return std::nullopt;
            return {FileHandle(raw_andle)};
          }
          
          // для c++23
          static std::expected<FileHandle, Error> FileHandle::create(fs::path path) {
            auto raw_handle = open(path);
            if (!raw_handle) return InvalidFilePathError{};
            return {FileHandle(raw_andle)};
          }

          Вариант 2. Отделить шаг валидации и кидать исключение извне.

          FileHandle::FileHandle(fs::path path) { 
            file = fs::open(path);
          }
          bool FileHandle::is_valid() { // ну или оператор каста перегрузить
            return bool(file);
          }
          
          ...
          
          auto fh = FileHandle(err_path);
          if (!fh.is_valid()) throw InvalidFilePathException{};

          Вариант 3. Двуступенчатая инициализация. Практически вариант 2, только логика проверки остаётся инкапсулирована в каком-нибудь bool init().


          1. Tuxman
            19.12.2025 09:31

            Библиотека https://github.com/TartanLlama/expected позволяет использовать std:expected ещё с C++11, до официального появления в стандарте C++23.


          1. eao197
            19.12.2025 09:31

            Да уж, какой только херни люди не придумают.

            Способы с функциями-фабриками, возвращающими что-то вроде std::expected, полностью оправданы если:

            • исключения полностью под запретом;

            • эти самые функции находятся на hot-path и вероятность выброса исключения из конструктора слишком высока. Поэтому приходиться избегать выброса исключений по соображениям эффективности.

            А вот вариант когда конструктор завершает свою работу, но валидность созданного объекта затем нужно проверять дополнительными методами is_valid -- это вообще порождение какого-то мрачного гения. Задача конструктора сформировать объект в валидном состоянии, если это невозможно, то объекта не должно быть, что и обеспечивается выбросом исключения. Т.е. либо у нас на выходе объект с выполненными инвариантами, либо исключение о невозможности эти самые инварианты выполнить. Все просто и логично.

            Если же мы допускаем появление объекта с нарушенными инвариантами, то мы закладываем бомбу замедленного действия. Рано или поздно кто-то на эти грабли наступит.


            1. domix32
              19.12.2025 09:31

              Вы спросили мои предложения - я вам предоставил. Ну и раз заговорили про инварианты - мне не нравится логика, когда мы исходя из сломанных объектов пытаемся собрать валидный объект. Поэтому считаю оптимальным сначала проверить инварианты, а уже потом конструировать что-то из валидных данных. Ну а для hot path так и вовсе сначала б данные подготавливал, а не исключения кидал, извращая поток данных.


              1. eao197
                19.12.2025 09:31

                Вы спросили мои предложения - я вам предоставил.

                Справедливости ради, не я спрашивал у вас предложения.

                Что бы лично я спросил, так это:

                А уж как замечательно кидать исключения в конструкторе

                откуда такое негативное отношение к исключениям из конструктора.

                Ну и раз заговорили про инварианты - мне не нравится логика, когда мы исходя из сломанных объектов пытаемся собрать валидный объект.

                А откуда эта попытка возьмется, если конструкторы будут бросать исключения при невозможности корректной инициализации объекта?

                Ну а для hot path так и вовсе сначала б данные подготавливал, а не исключения кидал, извращая поток данных.

                Собственно я об этом и говорил перечисляя случаи когда порождающая функция-фабрика с std::expected оправдана. Не вижу противоречия.


                1. domix32
                  19.12.2025 09:31

                  не я спрашивал у вас предложения.

                  fair point.

                  откуда такое негативное отношение к исключениям из конструктора.

                  Скорее к исключениям в принципе как источнику неявно отстрелить себе ногу. Особенно когда многопоточность появляется ловить исключения и отлаживать программу становится крайне неприятно. Не считая прочих проблем поменьше типа увеличения уровня вложенности кода.

                  А откуда эта попытка возьмется, если конструкторы будут бросать исключения при невозможности корректной инициализации объекта?

                  ну так в примере они пытаются сконструировать объект, но из-за исключения попытка проваливается.

                  когда порождающая функция-фабрика с std::expected оправдана

                  оно может быть и вовсе без опционалов. что-нибудь типа

                  std::vector<fs::path> paths;
                  paths
                    | filter_map([](auto& path) { return fs::open(path);       })
                    | transform([](auto&& path) { return FileDescriptor(path); })
                    | to<std::vector<FileDecriptor>>();


                  1. eao197
                    19.12.2025 09:31

                    Скорее к исключениям в принципе как источнику неявно отстрелить себе ногу.

                    Ну т.е. дело вовсе не в конструкторах, а в исключениях.

                    оно может быть и вовсе без опционалов. что-нибудь типа

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


      1. pavlushk0
        19.12.2025 09:31

        А как ещё просигналить ошибку в конструкторе? Наверн errno???


    1. ph_piter Автор
      19.12.2025 09:31

      Добрый день!
      Убрали некорректный хаб. Благодарим вас за обратную связь!

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

      Посмотрите, пожалуйста, исправленное форматирование в листинге с FileHandle
      Следует ли нам внести еще исправления или это то, что вы имели ввиду?


  1. voldemar_d
    19.12.2025 09:31

    В первом примере лучше запретить пустой конструктор:

    FileHandle() = delete;


    1. deelayka
      19.12.2025 09:31

      В первом примере есть проблемы посерьёзнее, чем удаление конструктора по умолчанию, который и так не будет создан.


    1. naviUivan
      19.12.2025 09:31

      Почему лучше? Там есть конструктор с аргументом, следовательно конструктор по умолчанию сгенерирован не будет.


      1. voldemar_d
        19.12.2025 09:31

        Вы уже попробовали скомпилировать такой код FileHandle fh;

        И как, не компилируется?


        1. deelayka
          19.12.2025 09:31

          Не компилируется. А у вас компилируется?


          1. voldemar_d
            19.12.2025 09:31

            Да, согласен. Не должно компилироваться.


      1. voldemar_d
        19.12.2025 09:31

        Не будет. Имхо, если пустой конструктор для нормальной работы класса не должен быть предусмотрен, лучше явно показать, что он запрещен. Чтобы потом кто-нибудь другой при рефакторинге кода его не добавил из каких-то соображений.


        1. deelayka
          19.12.2025 09:31

          Я и пытаюсь донести, что это уже дополнительные ограничения на случай возможных изменений в будущем. И то я бы не стал так делать. А вот то, что попытка скопировать или переместить объект приведёт к закрытию файла, этот пример никак не учитывает. Как и то, что передача nullptr в std::fclose является UB.