Допустимое использование goto для управления ошибками в C? - PullRequest
85 голосов
/ 25 апреля 2009

Этот вопрос на самом деле является результатом интересного обсуждения на сайте software.reddit.com. В основном это сводится к следующему коду:

int foo(int bar)
{
    int return_value = 0;
    if (!do_something( bar )) {
        goto error_1;
    }
    if (!init_stuff( bar )) {
        goto error_2;
    }
    if (!prepare_stuff( bar )) {
        goto error_3;
    }
    return_value = do_the_thing( bar );
error_3:
    cleanup_3();
error_2:
    cleanup_2();
error_1:
    cleanup_1();
    return return_value;
}

Использование goto здесь, кажется, лучший путь, в результате чего получается самый чистый и эффективный код из всех возможных, или, по крайней мере, мне так кажется. Цитировать Стива Макконнелла в Код завершен :

Goto полезно в рутине, которая распределяет ресурсы, выполняет операции на этих ресурсах, и затем освобождает ресурсы. С Goto, вы можете убрать в одном разделе кода. Goto уменьшает вероятность того, что вы забудете освободить ресурсы в каждом месте вы обнаружите ошибку.

Другая поддержка этого подхода содержится в книге Драйверы устройств Linux , в этом разделе .

Что ты думаешь? Является ли этот случай допустимым для goto в C? Вы бы предпочли другие методы, которые производят более запутанный и / или менее эффективный код, но избегают goto?

Ответы [ 14 ]

53 голосов
/ 26 апреля 2009

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

16 голосов
/ 25 апреля 2009

Как правило, хорошая идея - избегать goto, но злоупотребления, которые были распространены, когда Дейкстра впервые написал «GOTO рассмотрено как вредное», в наши дни даже не приходят в голову большинству людей.

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

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

int foo(int bar)
{
    int return_value = 0;
    if (!do_something(bar)) {
        goto error_1;
    }
    if (!init_stuff(bar)) {
        goto error_2;
    }
    if (prepare_stuff(bar))
    {
        return_value = do_the_thing(bar);
        cleanup_3();
    }
error_2:
    cleanup_2();
error_1:
    cleanup_1();
    return return_value;
}

Продолжение процесса:

int foo(int bar)
{
    int return_value = 0;
    if (do_something(bar))
    {   
        if (init_stuff(bar))
        {
            if (prepare_stuff(bar))
            {
                return_value = do_the_thing(bar);
                cleanup_3();
            }
            cleanup_2();
        }
        cleanup_1();
    }
    return return_value;
}

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

15 голосов
/ 14 июня 2011

Я удивлен, что никто не предложил эту альтернативу, поэтому, хотя вопрос уже давно, я добавлю его: один хороший способ решения этой проблемы - использовать переменные для отслеживания текущего состояния. Это метод, который можно использовать независимо от того, используется goto для получения кода очистки. Как и любой метод кодирования, у него есть свои плюсы и минусы, и он не подходит для любой ситуации, но если вы выбираете стиль, который стоит рассмотреть, особенно если вы хотите избежать goto, не заканчивая глубоко вложенными if s.

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

Сначала я покажу версию goto, потому что она ближе к коду в исходном вопросе.

int foo(int bar)
{
    int return_value = 0;
    int something_done = 0;
    int stuff_inited = 0;
    int stuff_prepared = 0;


    /*
     * Prepare
     */
    if (do_something(bar)) {
        something_done = 1;
    } else {
        goto cleanup;
    }

    if (init_stuff(bar)) {
        stuff_inited = 1;
    } else {
        goto cleanup;
    }

    if (prepare_stuff(bar)) {
        stufF_prepared = 1;
    } else {
        goto cleanup;
    }

    /*
     * Do the thing
     */
    return_value = do_the_thing(bar);

    /*
     * Clean up
     */
cleanup:
    if (stuff_prepared) {
        unprepare_stuff();
    }

    if (stuff_inited) {
        uninit_stuff();
    }

    if (something_done) {
        undo_something();
    }

    return return_value;
}

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

Теперь некоторые могут утверждать, что этот метод добавляет множество дополнительных переменных - и действительно, в этом случае это правда - но на практике часто существующая переменная уже отслеживает или может быть создана для отслеживания требуемого состояния. Например, если prepare_stuff() на самом деле является вызовом malloc() или open(), то можно использовать переменную, содержащую возвращенный указатель или дескриптор файла - например:

int fd = -1;

....

fd = open(...);
if (fd == -1) {
    goto cleanup;
}

...

cleanup:

if (fd != -1) {
    close(fd);
}

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

int foo(int bar)
{
    int return_value = 0;
    int something_done = 0;
    int stuff_inited = 0;
    int stuff_prepared = 0;
    int oksofar = 1;


    /*
     * Prepare
     */
    if (oksofar) {  /* NB This "if" statement is optional (it always executes) but included for consistency */
        if (do_something(bar)) {
            something_done = 1;
        } else {
            oksofar = 0;
        }
    }

    if (oksofar) {
        if (init_stuff(bar)) {
            stuff_inited = 1;
        } else {
            oksofar = 0;
        }
    }

    if (oksofar) {
        if (prepare_stuff(bar)) {
            stuff_prepared = 1;
        } else {
            oksofar = 0;
        }
    }

    /*
     * Do the thing
     */
    if (oksofar) {
        return_value = do_the_thing(bar);
    }

    /*
     * Clean up
     */
    if (stuff_prepared) {
        unprepare_stuff();
    }

    if (stuff_inited) {
        uninit_stuff();
    }

    if (something_done) {
        undo_something();
    }

    return return_value;
}

Снова, есть потенциальная критика этого:

  • Разве все эти "если" не повредили производительности? Нет - потому что в случае успеха вы все равно должны выполнить все проверки (иначе вы не проверяете все случаи ошибок); и в случае сбоя большинство компиляторов оптимизируют последовательность неудачных проверок if (oksofar) для одного перехода к коду очистки (конечно, GCC делает) - и в любом случае, ошибка обычно менее критична для производительности.
  • Разве это не добавляет еще одну переменную? В этом случае да, но часто переменную return_value можно использовать для того, чтобы играть роль, которую играет здесь oksofar. Если вы структурируете свои функции для последовательного возврата ошибок, вы даже можете избежать второго if в каждом случае:

    int return_value = 0;
    
    if (!return_value) {
        return_value = do_something(bar);
    }
    
    if (!return_value) {
        return_value = init_stuff(bar);
    }
    
    if (!return_value) {
        return_value = prepare_stuff(bar);
    }
    

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

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

8 голосов
/ 25 апреля 2009

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

FWIW, если вы посмотрите учебники developer.apple.com, они используют подход goto к обработке ошибок.

Мы не используем gotos. Более важное значение уделяется возвращаемым значениям. Обработка исключений осуществляется через setjmp/longjmp - все, что вы можете.

4 голосов
/ 25 апреля 2009

Нет ничего морально неправильного в выражении goto, кроме как что-то морально неправильное с указателями (void) *.

Все дело в том, как вы используете инструмент. В представленном вами (тривиальном) случае оператор case может реализовать ту же логику, хотя и с большими накладными расходами. На самом деле вопрос в том, каково мое требование к скорости?

goto просто быстр, особенно если вы заботитесь, чтобы он компилировался в короткий прыжок. Идеально подходит для приложений, где скорость является премией. Для других приложений, вероятно, имеет смысл использовать издержки if / else + case для удобства обслуживания.

Помните: goto не убивает приложения, разработчики убивают приложения.

ОБНОВЛЕНИЕ: Вот пример случая

int foo(int bar) { 
     int return_value = 0 ; 
     int failure_value = 0 ;

     if (!do_something(bar)) { 
          failure_value = 1; 
      } else if (!init_stuff(bar)) { 
          failure_value = 2; 
      } else if (prepare_stuff(bar)) { 
          return_value = do_the_thing(bar); 
          cleanup_3(); 
      } 

      switch (failure_value) { 
          case 2: cleanup_2(); 
          case 1: cleanup_1(); 
          default: break ; 
      } 
} 
2 голосов
/ 26 апреля 2009

GOTO полезно. Это то, что ваш процессор может сделать, и именно поэтому вы должны иметь к нему доступ.

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

1 голос
/ 31 октября 2015

Я согласен, что очистка goto в обратном порядке, приведенная в вопросе, является самым чистым способом очистки вещей в большинстве функций. Но я также хотел отметить, что иногда вы хотите, чтобы ваша функция все-таки очистилась. В этих случаях я использую следующий вариант, если if (0) {label:} идиома, чтобы перейти к правильной точке процесса очистки:

int decode ( char * path_in , char * path_out )
{
  FILE * in , * out ;
  code c ;
  int len ;
  int res = 0  ;
  if ( path_in == NULL )
    in = stdin ;
  else
    {
      if ( ( in = fopen ( path_in , "r" ) ) == NULL )
        goto error_open_file_in ;
    }
  if ( path_out == NULL )
    out = stdout ;
  else
    {
      if ( ( out = fopen ( path_out , "w" ) ) == NULL )
        goto error_open_file_out ;
    }

  if( read_code ( in , & c , & longueur ) )
    goto error_code_construction ;

  if ( decode_h ( in , c , out , longueur ) )
  goto error_decode ;

  if ( 0 ) { error_decode: res = 1 ;}
  free_code ( c ) ;
  if ( 0 ) { error_code_construction: res = 1 ; }
  if ( out != stdout ) fclose ( stdout ) ;
  if ( 0 ) { error_open_file_out: res = 1 ; }
  if ( in != stdin ) fclose ( in ) ;
  if ( 0 ) { error_open_file_in: res = 1 ; }
  return res ;
 }
1 голос
/ 06 июня 2012

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

  do {
    .. set up thing1 that will need cleanup only in case of early exit
    if (error) break;
    do
    {
      .. set up thing2 that will need cleanup in case of early exit
      if (error) break;
      // ***** SEE TEXT REGARDING THIS LINE
    } while(0);
    .. cleanup thing2;
  } while(0);
  .. cleanup thing1;

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

В сценарии «очистки даже в обычном случае» я бы рассматривал использование goto как более понятное, чем конструкции do / while(0), в том числе потому, что сами метки назначения практически выкрикивают «LOOK» AT ME "гораздо больше, чем конструкции break и do / while(0). В случае «очистки только в случае ошибки» оператор return в конечном итоге должен оказаться в самом худшем из возможных мест с точки зрения читабельности (операторы возврата обычно должны быть либо в начале функции, либо в другом месте) выглядит как «конец»; наличие return непосредственно перед тем, как целевой ярлык встречает эту квалификацию гораздо легче, чем наличие его непосредственно перед окончанием «цикла».

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

 REPARSE_PACKET:
  switch(packet[0])
  {
    case PKT_THIS_OPERATION:
      if (problem condition)
        goto PACKET_ERROR;
      ... handle THIS_OPERATION
      break;
    case PKT_THAT_OPERATION:
      if (problem condition)
        goto PACKET_ERROR;
      ... handle THAT_OPERATION
      break;
    ...
    case PKT_PROCESS_CONDITIONALLY
      if (packet_length < 9)
        goto PACKET_ERROR;
      if (packet_condition involving packet[4])
      {
        packet_length -= 5;
        memmove(packet, packet+5, packet_length);
        goto REPARSE_PACKET;
      }
      else
      {
        packet[0] = PKT_CONDITION_SKIPPED;
        packet[4] = packet_length;
        packet_length = 5;
        packet_status = READY_TO_SEND;
      }
      break;
    ...
    default:
    {
     PACKET_ERROR:
      packet_error_count++;
      packet_length = 4;
      packet[0] = PKT_ERROR;
      packet_status = READY_TO_SEND;
      break;
    }
  }   

Хотя можно заменить операторы goto на {handle_error(); break;}, и хотя можно использовать цикл do / while(0) вместе с continue для обработки обернутого пакета условного выполнения, я не очень думаю, что это понятнее, чем использовать goto. Кроме того, хотя возможно копировать код из PACKET_ERROR везде, где используется goto PACKET_ERROR, и в то время как компилятор может вывести дублированный код один раз и заменить большинство вхождений переходом к этой общей копии, используя goto облегчает обнаружение мест, которые устанавливают пакет немного по-другому (например, если инструкция «выполнить условно» решает не выполнять).

1 голос
/ 27 апреля 2009

Я лично являюсь последователем "Сила десяти - 10 правил написания критического кода безопасности" .

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


Правило: ограничьте весь код очень простыми конструкциями потока управления - не используйте goto операторы, конструкции setjmp или longjmp и прямая или косвенная рекурсия.

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


Запрет использования goto кажется плохим , но:

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

0 голосов
/ 30 сентября 2016

Вот что я предпочел:

bool do_something(void **ptr1, void **ptr2)
{
    if (!ptr1 || !ptr2) {
        err("Missing arguments");
        return false;
    }
    bool ret = false;

    //Pointers must be initialized as NULL
    void *some_pointer = NULL, *another_pointer = NULL;

    if (allocate_some_stuff(&some_pointer) != STUFF_OK) {
        err("allocate_some_stuff step1 failed, abort");
        goto out;
    }
    if (allocate_some_stuff(&another_pointer) != STUFF_OK) {
        err("allocate_some_stuff step 2 failed, abort");
        goto out;
    }

    void *some_temporary_malloc = malloc(1000);

    //Do something with the data here
    info("do_something OK");

    ret = true;

    // Assign outputs only on success so we don't end up with
    // dangling pointers
    *ptr1 = some_pointer;
    *ptr2 = another_pointer;
out:
    if (!ret) {
        //We are returning an error, clean up everything
        //deallocate_some_stuff is a NO-OP if pointer is NULL
        deallocate_some_stuff(some_pointer);
        deallocate_some_stuff(another_pointer);
    }
    //this needs to be freed every time
    free(some_temporary_malloc);
    return ret;
}
...