Пара слов в защиту goto

Все программисты знают, что использовать оператор goto — моветон. Объясняют это обычно тем, что он сильно запутывает код и делает его трудно читаемым. Это так. Но в некоторых случаях с помощью него можно избежать нежелательного дублирования кода.

Это происходит при создании сложного объекта, когда для получения конечного результата необходимо выполнить ряд вспомогательных действий, каждое из которых может завершиться ошибкой. При этом после каждого вспомогательного действия нужно освобождать ресурсы. Наглядным примером такого сложного объекта является проекция файла (FileMap) в Windows API. Его создание состоит из нескольких этапов:

1. Открытие файла, который будет проецироваться.
2. Проверка размера файла.
3. Создание объекта «проекция файла».
4. Проецирование файла.

Каждый из этих этапов может закончиться ошибкой, в случае которой дальнейшие лишены смысла. Так же после каждого шага (кроме 2) нужно освобождать ресурсы (закрывать описатели соответствующих объектов ядра). В принципе второй шаг необязателен. Если указан пустой файл, то на шаге 4 выскочит ошибка. С другой стороны в некоторых случаях размер файла может быть важен (все зависит от логики вашего приложения), поэтому я его оставил. Ниже приводится возможная реализация этого алгоритма.

HANDLE hfile = INVALID_HANDLE_VALUE;
DWORD fileSize = 0;
const DWORD MIN_FILE_SIZE = 100;
HANDLE hfileMap = NULL;
PBYTE buffer = NULL;

bool InitFileMap()
{
  //Открываем файл с данными
  hfile = CreateFile(TEXT("DataFile.dat"), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  if (hfile == INVALID_HANDLE_VALUE)
    return false;
  
  //Проверяем размер данных
  fileSize = GetFileSize(hfile, NULL);
  if (fileSize < MIN_FILE_SIZE)
  {
    CloseHandle(hfile);
    hfile = INVALID_HANDLE_VALUE;
    return false;
  }

  //Создаем объект проекция файла
  hfileMap = CreateFileMapping(hfile, NULL, PAGE_READONLY, 0, fileSize, NULL);
  if (!hfileMap)
  {
    fileSize = 0;
    CloseHandle(hfile);
    hfile = INVALID_HANDLE_VALUE;
    return false;
  }

  //Проецируем файл
  buffer = (PBYTE)MapViewOfFile(hfileMap, FILE_MAP_READ, 0, 0, 0);
  if (!buffer)
  {
    CloseHandle(hfileMap);
    hfileMap = NULL;
    fileSize = 0;
    CloseHandle(hfile);
    hfile = INVALID_HANDLE_VALUE;
    return false;
  }

  return true;
}

Данная реализация не претендует на «чистоту», она лишь показывает идею. Обратите внимание: последовательность команд

CloseHandle(hfile);
hfile = INVALID_HANDLE_VALUE;
return false;

Повторяется трижды. При этом ни одну из них нельзя назвать «лишней». В вашем приложении при построении более сложных объектов таких повторений может быть больше. Можно ли их как-то уменьшить? Да, но для этого нужно использовать goto.

HANDLE hfile = INVALID_HANDLE_VALUE;
DWORD fileSize = 0;
const DWORD MIN_FILE_SIZE = 100;
HANDLE hfileMap = NULL;
PBYTE buffer = NULL;

bool InitFileMap()
{
  //Открываем файл с данными
  hfile = CreateFile(TEXT("DataFile.dat"), GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  if (hfile == INVALID_HANDLE_VALUE)
    return false;
  
  //Проверяем размер данных
  fileSize = GetFileSize(hfile, NULL);
  if (fileSize < MIN_FILE_SIZE)
    goto m_error;

  //Создаем объект проекция файла
  hfileMap = CreateFileMapping(hfile, NULL, PAGE_READONLY, 0, fileSize, NULL);
  if (!hfileMap)
    goto m_error;
 
  //Проецируем файл
  buffer = (PBYTE)MapViewOfFile(hfileMap, FILE_MAP_READ, 0, 0, 0);
  if (!buffer)
    goto m_error;

  return true;

m_error:
  if (hfileMap)
  CloseHandle(hfileMap);

  hfileMap = NULL;
  fileSize = 0;
  CloseHandle(hfile);
  hfile = INVALID_HANDLE_VALUE;
  return false;
}

Обратите внимание: мы полностью избавились от дублирования за счет использования goto. Код стал даже несколько проще. Но это goto!!!

Другим примером когда он может быть полезен — это быстрая доработка программы, когда нужно попробовать какой-то новый вариант (не)вашего алгоритма, а полностью и основательно переделывать приложение нет времени. Нужно внести небольшую правку. Причем нет гарантии что вам потом не понадобится возвращать все к первоначальному виду. В этом случае goto можно использовать в качестве некоего «костыля». Конечно в последствии приложение нужно адаптировать под новую версию алгоритма и убрать этот «костыль». Но для быстрой проверки идеи он вполне сойдет.

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

Один ответ

  1. goto в C++ не совместим с исключениями.
    Если во время выполнения выбросится исключение, то goto не выполнится и ресурсы не освободятся.
    Правильный способ работы с ресурсами это обернуть их в класс деструктор которого освободит ресурсы даже если будет выброшено исключение

Добавить комментарий для Vvv Отменить ответ

Ваш адрес email не будет опубликован. Обязательные поля помечены *