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