Макросы управления потоком с помощью 'goto' - PullRequest
6 голосов
/ 31 марта 2009

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

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

void func()
{
   char* p1 = malloc(16);
   if( !p1 )
      goto cleanup;

   char* p2 = malloc(16);
   if( !p2 )
      goto cleanup;

 goto norm_cleanup;

 err_cleanup:

   if( p1 )
      free(p1);

   if( p2 )
      free(p2);

 norm_cleanup:
}

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

void func()
{
   char* p1 = malloc(16);
   if( !p1 ){
      return;
   }

   char* p2 = malloc(16);
   if( !p2 ){
      free(p1);
      return;
   }

   char* p3 = malloc(16);
   if( !p3 ){
      free(p1);
      free(p2);
      return;
   }
}

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

Итак, чтобы иметь возможность использовать goto, но при этом четко изолировать его от свободного использования, для обработки задачи был создан набор макросов управления потоком. Выглядит примерно так (упрощенно):

#define FAIL_SECTION_BEGIN int exit_code[GUID] = 0;
#define FAIL_SECTION_DO_EXIT_IF( cond, exitcode ) if(cond){exit_code[GUID] = exitcode; goto exit_label[GUID];}
#define FAIL_SECTION_ERROR_EXIT(code) exit_label[GUID]: if(exit_code[GUID]) int code = exit_code[GUID];else goto end_label[GUID]
#define FAIL_SECTION_END end_label[GUID]:

Мы можем использовать это следующим образом:

int func()
{
   char* p1 = NULL;
   char* p2 = NULL;
   char* p3 = NULL;

   FAIL_SECTION_BEGIN
   {
      p1 = malloc(16);
      FAIL_SECTION_DO_EXIT_IF( !p1, -1 );

      p2 = malloc(16);
      FAIL_SECTION_DO_EXIT_IF( !p2, -1 );

      p3 = malloc(16);
      FAIL_SECTION_DO_EXIT_IF( !p3, -1 );
   }
   FAIL_SECTION_ERROR_EXIT( code )
   {
      if( p3 ) 
         free(p3);

      if( p2 ) 
         free(p2);

      if( p1 ) 
         free(p1);

      return code;
    }
    FAIL_SECTION_END

  return 0;

Это выглядит красиво и дает много преимуществ, НО, есть ли какие-то недостатки, о которых нам следует подумать, прежде чем внедрять это в разработку? В конце концов, это очень управление потоком и goto: ish. Оба обескуражены. Каковы аргументы против их в этом случае?

Спасибо.

Ответы [ 9 ]

11 голосов
/ 31 марта 2009

Обработка ошибок - одна из редких ситуаций, когда goto не так уж и плох.

Но если бы мне пришлось поддерживать этот код, я бы очень расстроился, что goto скрыты макросами.

Так что в этом случае goto нормально для меня, но не для макросов.

7 голосов
/ 31 марта 2009

Этот код:

void func()
{
   char* p1 = malloc(16);
   if( !p1 )
      goto cleanup;

   char* p2 = malloc(16);
   if( !p2 )
      goto cleanup;

 cleanup:

   if( p1 )
      free(p1);

   if( p2 )
      free(p2);
}

может быть юридически записано как:

void func()
{
   char* p1 = malloc(16);
   char* p2 = malloc(16);

    free(p1);
    free(p2);
}

независимо от того, успешно ли выделены памяти.

Это работает, потому что free () ничего не делает, если передан NULL-указатель. Вы можете использовать ту же идиому при разработке своих собственных API для выделения и освобождения других ресурсов:

// return handle to new Foo resource, or 0 if allocation failed
FOO_HANDLE AllocFoo();

// release Foo indicated by handle, - do nothing if handle is 0
void ReleaseFoo( FOO_HANDLE h );

Разработка таких API-интерфейсов может значительно упростить управление ресурсами.

7 голосов
/ 31 марта 2009

Использование goto для перехода к общему обработчику ошибок / последовательности очистки / выхода абсолютно нормально.

3 голосов
/ 31 марта 2009

Если первый malloc не сработает, очистите оба p1 и p2. Из-за перехода, p2 не инициализируется и может указывать на что-либо. Я быстро запустил это с gcc для проверки, и попытка освобождения (p2) действительно вызовет ошибку сегмента.

В вашем последнем примере переменные находятся в фигурных скобках (т.е. они существуют только в блоке FAIL_SECTION_BEGIN).

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

Я ничего не имею против goto и макросов, но предпочитаю идею Нила Баттерворта.

void func(void)
{
    void *p1 = malloc(16);
    void *p2 = malloc(16);
    void *p3 = malloc(16);

    if (!p1 || !p2 || !p3) goto cleanup;

    /* ... */

cleanup:
    if (p1) free(p1);
    if (p2) free(p2);
    if (p3) free(p3);
}

Или, если это более уместно ..

void func(void)
{
    void *p1 = NULL;
    void *p2 = NULL;
    void *p3 = NULL;

    p1 = malloc(16);
    if (!p1) goto cleanup;

    p2 = malloc(16);
    if (!p2) goto cleanup;

    p3 = malloc(16);
    if (!p3) goto cleanup;

    /* ... */

cleanup:
    if (p1) free(p1);
    if (p2) free(p2);
    if (p3) free(p3);
}
3 голосов
/ 31 марта 2009

Очистка с помощью goto является распространенной идиомой языка C и используется в ядре Linux *.

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

2 голосов
/ 31 марта 2009

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

void func()
{
    char *p1 = 0;
    char *p2 = 0;
    char *p3 = 0;

    if ((p1 = malloc(16)) != 0 &&
        (p2 = malloc(16)) != 0 &&
        (p3 = malloc(16)) != 0)
    {
        // Use p1, p2, p3 ...
    }
    free(p1);
    free(p2);
    free(p3);
}

Когда после каждой операции выделения есть нетривиальные объемы работы, вы можете использовать метку перед первой из операций free(), и goto в порядке - обработка ошибок является основной причиной использования goto в наши дни, и что-то еще несколько сомнительно.

Я смотрю за кодом, в котором есть макросы со встроенными операторами goto. При первом знакомстве сбивает с толку возможность увидеть метку, на которую «не ссылается» видимый код, но которую нельзя удалить. Я предпочитаю избегать таких практик. Макросы в порядке, когда мне не нужно знать, что они делают - они просто делают это. Макросы не так хороши, когда вы должны знать, что они расширяют, чтобы использовать их точно. Если они не скрывают информацию от меня, это скорее неприятность, чем помощь.

Иллюстрация - имена, замаскированные для защиты виновных:

#define rerrcheck if (currval != &localval && globvar->currtub &&          \
                    globvar->currtub->te_flags & TE_ABORT)                 \
                    { if (globvar->currtub->te_state)                      \
                         globvar->currtub->te_state->ts_flags |= TS_FAILED;\
                      else                                                 \
                         delete_tub_name(globvar->currtub->te_name);       \
                      goto failure;                                        \
                    }


#define rgetunsigned(b) {if (_iincnt>=2)  \
                           {_iinptr+=2;_iincnt-=2;b = ldunsigned(_iinptr-2);} \
                         else {b = _igetunsigned(); rerrcheck}}

На rgetunsigned() есть несколько десятков вариантов, которые несколько похожи - разные размеры и разные функции загрузчика.

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

        for (i = 0 ; i < no_of_rows; i++)
            {
            row_t *tmprow = &val->v_coll.cl_typeinfo->clt_rows[i];

            rgetint(tmprow->seqno);
            rgetint(tmprow->level_no);
            rgetint(tmprow->parent_no);
            rgetint(tmprow->fieldnmlen);
            rgetpbuf(tmprow->fieldname, IDENTSIZE);
            rgetint(tmprow->field_no);
            rgetint(tmprow->type);
            rgetint(tmprow->length);
            rgetlong(tmprow->xid);
            rgetint(tmprow->flags);
            rgetint(tmprow->xtype_nm_len);
            rgetpbuf(tmprow->xtype_name, IDENTSIZE);
            rgetint(tmprow->xtype_owner_len);
            rgetpbuf(tmprow->xtype_owner_name, IDENTSIZE);
            rgetpbuf(tmprow->xtype_owner_name,
                     tmprow->xtype_owner_len);
            rgetint(tmprow->alignment);
            rgetlong(tmprow->sourcetype);
            }

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

2 голосов
/ 31 марта 2009

Термин «структурированное программирование», который все мы знаем как анти-goto, изначально возник и развивался как набор шаблонов кодирования с goto (или JMP). Эти паттерны были названы паттернами while и if, среди прочих.

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

1 голос
/ 31 марта 2009

Первый пример выглядит намного более читабельным для меня, чем макрокомандная версия. И Мувициель сказал это намного лучше, чем я

0 голосов
/ 23 апреля 2009
#define malloc_or_die(size) if(malloc(size) == NULL) exit(1)

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

Для реального примера правильного использования goto, проверьте код отправки, который использует вычисленное goto.

...