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

      Комментарии к записи Ответ в теме: Идиома RAII. Объекты управления ресурсами в C++ отключены
#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, например. Все эти классы захватывают ресурс в конструкторе, работают с ним и освобождают его в деструкторе.