Идиома RAII. Объекты управления ресурсами в C++

      Комментарии к записи Идиома RAII. Объекты управления ресурсами в C++ отключены

Главная Форумы Программирование Программирование на С++ Учебные материалы по С++ Идиома RAII. Объекты управления ресурсами в C++

Помечено: , ,

В этой теме 3 ответа, 2 участника, последнее обновление  Васильев Владимир Сергеевич 1 год, 1 месяц назад.

  • Автор
    Сообщения
  • #2830

    questioner
    Участник

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

  • #2850

    Идиома RAII (Resource Acquisition is Initialization) обеспечивает безопасную работу с любыми ресурсами — например файлами, сетевыми соединениями, памятью. Заключается в использовании объектов управления ресурсами, которые:

    1. захватывают ресурс в конструкторе;
    2. освобождают ресурс в деструкторе;
    3. создаются на стеке (деструктор будет автоматически вызван при раскручивании стека;

    Допустим, в нашей программе есть функция, которая позволяет пользователю редактировать файл. Функция выводит меню (что можно сделать с файлом), а пользователь выбирает нужный ему пункт. Код специально написан так, чтобы показать проблемы:

    void text_edit(const char *filename) {
      FILE *file = fopen (filename,"rw+");
      int menu_point;
      while (true) {
        printf("enter:\n\t0 - exit\n");
        printf("enter:\n\t1 - remove double spaces in the file\n");
        scanf("%d", &menu_point);
    
        switch (menu_point) {
        case 0:
          fclose(file);
          return;
        case 1:
          remove_double_spaces(file);
          break;
        default:
          printf("bad menu point");
          return;
        }
      }
    }

    Я думаю, многим этот код напомнит одну из их первых программ, однако, в нем есть множество проблем. Скорее всего, автор кода хотел, чтобы пользователь не
    смог выйти из цикла иначе, чем через выбор опции "exit", которая приведет к корректному закрытию файла. Однако есть, как минимум, два случая, когда ресурс после завершения работы функции останется захваченным (при этом никакое приложение не сможет открыть файл и даже удалить его до тех пор, пока не завершится процесс нашей программы):

    • если пользователь введет несуществующий пункт меню — функция завершится без закрытия файла (в ветке default). Аналогичная проблема возникнет если пользователь введет не целое число или строку в качестве пункта;
    • если функция remove_double_spaces выработает исключение.

    Обе проблемы можно решить вручную:

    1. добавить обработку кода ошибки, возвращаемого scanf;
    2. в обработчике некорректного номера пункта добавить закрытие файла;
    3. поместить вызов функции, возвращающей исключение внутрь блока try и добавить соответствующий возможному типу исключения catch-блок;

    Достаточно сложно контролировать все возможные точки выхода из функции и захваченные ресурсы:

    1. после нас мог бы прийти другой программист и добавить новый пункт меню (функцию), забыв про исключения;
    2. кто-нибудь мог бы изменить код remove_double_spaces так, что она начнет вырабатывать новый тип исключений;
    3. особые проблемы возникают если в одной функции захватывается несколько ресурсов:
        char *str_1 = nullptr, *str_2 = nullptr;
        while (true) {
          cin >> menu_point;
          switch (menu_point) {
          case 0:
            if (str_1)
              delete[] str_1;
            if (str_2)
              delete[] str_2;
            return;
          case 1:
            str_1 = new char[255];
            cin >> str_1;
            break;
          case 2:
            str_2 = new char[255];
            cin >> str_1;
            break;
          }
        }

      Мы не сможем просто вызвать delete[] для каждого указателя, ведь нам нужно сначала убедиться, что память по нему была выделена. В таких случаях типичной ошибкой является освобождение не захваченного ресурса. Кроме того, в этом примере оба оператор new могли бы кинуть исключения bad_alloc и (если разрешить пользователю вводить размер строки) — bad_array_new_length.

    Из приведенных примеров должно быть ясно, что ручное управление ресурсами — это очень сложно. RAII классы призваны упростить ваш код. Так, в стандартной библиотеке есть классы fstream и string, которые автоматически освобождают ресурс в деструкторе. Деструктор автоматически вызывается при выходе из функции если объект размещен на стеке.

    void text_edit(const char *filename) {
      fstream fst(filename, ios_base::in|ios_base::out);
      int menu_point;
      while (true) {
        printf("enter:\n\t0 - exit\n");
        printf("enter:\n\t1 - remove double spaces in the file\n");
        scanf("%d", &menu_point);
    
        switch (menu_point) {
        case 0:
          return;
        case 1:
          remove_double_spaces(file);
          break;
        default:
          printf("bad menu point");
          return;
        }
      }
    }

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

    Второй пример не имел особого смысла (он лишь демонстрировал проблемы, возникающие при захвате нескольких ресурсов), но его тоже можно переписать с использованием RAII-классов:

      string str_1, str_2;
      while (true) {
        cin >> menu_point;
        switch (menu_point) {
        case 0:
          return
        case 1:
          str_1.resize(255);
          cin >> str_1;
          break;
        case 2:
          str_1.resize(255);
          cin >> str_2;
          break;
        }
      }

    Советы по написанию собственных RAII-классов можно найти у Маерса в книге «Эффективное использование С++. 55 верных способов улучшить структуру и код ваших программ» (глава 3 — «Управление ресурсами»).

    Возвращаясь к вопросу, идиома RAII предлагает удобный и эффективный способ решения проблемы корректного освобождения ресурсов, в том числе, в случае возникновения исключений. Умные указатели — это RAII классы, как и string, vector и fstream, например. Все эти классы захватывают ресурс в конструкторе, работают с ним и освобождают его в деструкторе.

    • #2856

      questioner
      Участник

      Вы говорите, что стандартные классы string и fstream — это RAII классы, но не могли бы вы привести небольшой пример своего такого класса, чтобы посмотреть как именно он реализуется внутри?

      • #2857

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

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

        Если мы работаем с файлом, то именно операционная система выполняет большую часть работы работы по управлению ресурсом — она не позволяет открыть двум процессам одновременно файл для записи и т.д. Наш RAII класс сможет лишь вызвать функцию open в конструкторе и close в деструкторе.

        Если ресурсом является мьютекс — такой пример рассматривает Скотт Мэйерс:

        class Lock {
        public:
          explicit Lock(Mutex* mutex) : m_pmutex(mutex) {
            lock(m_pmutex);
          }
          ~Lock() {
            unlock(m_pmutex);
          }
        private:
          Mutex *m_pmutex;
        };

        То наш класс будет лишь вызывать функции lock (в конструкторе) и unlock (в деструкторе).

        Работа с сокетами (ресурсом является сетевое соединение) часто на уровне операционной системы эквивалентна работе с файлами, т.к. сокет на самом деле и представляет собой файл. Мы пишем в сокет данные, чтобы передать их на другой узел в сети, но на самом деле лишь дописывам их в конец файла, а другой процесс, занимающийся передачей — берет их из начала. Аналогично реализуется работа с именованными каналами (механизм взаимодействия процессов операционной системы) и много всего другого.

        При работе с оперативной памятью RAII-классы могут вызывать функции new и delete (или malloc/free) которые возвращают область памяти менеджеру. Существует огромное количество таких классов. В качестве примера собственного RAII-класса можно рассмотреть Array.

Для ответа в этой теме необходимо авторизоваться.