Подтвердить что ты не робот

Действительное использование goto для управления ошибками в C?

Этот вопрос на самом деле является результатом интересного обсуждения на сайте programming.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 здесь, по-видимому, является лучшим способом, что приводит к самому чистому и эффективному коду всех возможностей или, по крайней мере, так мне кажется. Цитата: Стив Макконнелл в Code Complete:

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

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

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

4b9b3361

Ответ 1

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

Ответ 2

Как правило, избегать 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;
}

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

Ответ 3

Я удивлен, что никто не предложил эту альтернативу, поэтому, хотя вопрос был примерно через некоторое время, я добавлю его: один хороший способ решить эту проблему - использовать переменные, чтобы отслеживать текущее состояние. Это метод, который можно использовать независимо от того, используется ли 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);
    }
    

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

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

Ответ 4

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

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

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

Ответ 5

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

Все это в том, как вы используете инструмент. В (тривиальном) случае, который вы представили, case 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 ; 
      } 
} 

Ответ 6

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

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

Ответ 7

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

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


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

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


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

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

Ответ 8

В целом, я хотел бы рассмотреть тот факт, что фрагмент кода может быть наиболее четко написан с использованием 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 упрощает замечать места, которые устанавливают пакет немного по-другому (например, если команда "выполнить условно" решает не выполнять).

Ответ 9

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

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 ;
 }

Ответ 10

Мне кажется, что cleanup_3 должен выполнить очистку, а затем вызвать cleanup_2. Аналогично, cleanup_2 должен выполнить очистку, а затем вызвать cleanup_1. Похоже, что в любое время, когда вы выполняете cleanup_[n], требуется cleanup_[n-1], поэтому он должен нести ответственность за метод (так что, например, cleanup_3 никогда нельзя вызвать без вызова cleanup_2 и, возможно, вызвать утечку.)

Учитывая этот подход, вместо gotos, вы просто вызываете процедуру очистки, а затем возвращаетесь.

Подход goto не является неправильным или плохим, однако стоит просто отметить, что это не обязательно "самый чистый" подход (IMHO).

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

Ответ 11

Я думаю, что вопрос здесь ошибочен в отношении данного кода.

Рассмотрим:

  • do_something(), init_stuff() и prepare_stuff(), похоже, знают, были ли они неудачными, поскольку в этом случае они возвращают либо false, либо nil.
  • Ответственность за настройку состояния, как представляется, несет ответственность за эти функции, поскольку в foo() не устанавливается состояние непосредственно.

Следовательно: do_something(), init_stuff() и prepare_stuff() должны выполнять свою собственную очистку. Имея отдельную функцию cleanup_1(), которая очищает после do_something(), нарушает философию инкапсуляции. Это плохая конструкция.

Если они сделали свою собственную очистку, то foo() станет довольно простым.

С другой стороны. Если foo() фактически создал свое собственное состояние, которое нужно было снести, то goto был бы уместным.

Ответ 12

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

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;
}

Ответ 13

Я предпочитаю использовать метод, описанный в следующем примере...

struct lnode *insert(char *data, int len, struct lnode *list) {
    struct lnode *p, *q;
    uint8_t good;
    struct {
            uint8_t alloc_node : 1;
            uint8_t alloc_str : 1;
    } cleanup = { 0, 0 };

    // allocate node.
    p = (struct lnode *)malloc(sizeof(struct lnode));
    good = cleanup.alloc_node = (p != NULL);

    // good? then allocate str
    if (good) {
            p->str = (char *)malloc(sizeof(char)*len);
            good = cleanup.alloc_str = (p->str != NULL);
    }

    // good? copy data
    if(good) {
            memcpy ( p->str, data, len );
    }

    // still good? insert in list
    if(good) {
            if(NULL == list) {
                    p->next = NULL;
                    list = p;
            } else {
                    q = list;
                    while(q->next != NULL && good) {
                            // duplicate found--not good
                            good = (strcmp(q->str,p->str) != 0);
                            q = q->next;
                    }
                    if (good) {
                            p->next = q->next;
                            q->next = p;
                    }
            }
    }

    // not-good? cleanup.
    if(!good) {
            if(cleanup.alloc_str)   free(p->str);
            if(cleanup.alloc_node)  free(p);
    }

    // good? return list or else return NULL
    return (good? list: NULL);

}

источник: http://blog.staila.com/?p=114

Ответ 14

Мы используем Daynix CSteps библиотеку в качестве другого решения для проблемы goto "в функциях init.
См. здесь и здесь.