Макросы управления потоком с помощью «goto»

Да, объединение двух ненавистных конструкций. Так ли это плохо, как кажется, или его можно рассматривать как хороший способ контролировать использование 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. Оба обескуражены. Каковы аргументы для их обескураживания в этом случае?

Спасибо.


person sharkin    schedule 31.03.2009    source источник
comment
Как указывает Мартин Фидо в своем ответе, в последнем фрагменте кода указатели p1, p2 и p3 находятся вне раздела обработки ошибок, а в первом фрагменте кода p2 будет содержать мусорные данные (если он вообще скомпилируется - - не уверен в правилах C).   -  person j_random_hacker    schedule 31.03.2009
comment
@j: я уверен, что этот код даже не пройдет через компилятор. Спасибо за подсказку, но вы, должно быть, упустили весь смысл вопроса.   -  person sharkin    schedule 01.04.2009
comment
@RA: С чего ты взял, что я упустил суть? Некомпилируемость была запутанной побочной проблемой, на которую, как мне показалось, стоит указать в комментарии.   -  person j_random_hacker    schedule 01.04.2009
comment
@j: Не беспокойтесь. Поскольку вы оба прокомментировали это здесь и проголосовали за сообщение Мартина Фидо с одинаковой мотивацией, это показалось вам чем-то большим, чем побочным.   -  person sharkin    schedule 01.04.2009
comment
@РА: Я понимаю, что ты имеешь в виду. В этом случае я только что заметил пост Мартина первым. Я склонен использовать комментарии внизу вопроса, чтобы сказать: «Эй, кстати, не могли бы вы пояснить XYZ…   -  person j_random_hacker    schedule 01.04.2009


Ответы (9)


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

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

Так что в данном случае мне подходит goto, но не макросы.

person mouviciel    schedule 31.03.2009
comment
редкая ситуация связана не с обработкой ошибок, а с использованием goto. - person mouviciel; 31.03.2009
comment
Я не возражаю против использования goto в макросе и делаю это время от времени, НО скрывать метку, на которую перешел макрос, абсолютно нельзя. т.е. не в порядке: #define CHECK_RESULT(res) if (res == 0) перейти к очистке;, ok: #define CHECK_RESULT(res, label) if (res == 0) goto label;. Таким образом, вы можете понять, что макрос прыгает (например, res = something(); CHECK_RESULT(res, cleanup); ... cleanup: ...). - person hlovdal; 31.05.2009

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

person GSerg    schedule 31.03.2009
comment
Но макросы я бы исключил. Ваши глазки программиста обучены работе с if(). Эти макросы появляются сравнительно редко, и поэтому их гораздо труднее читать. - person Carl Seleborg; 31.03.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 может значительно упростить управление ресурсами.

person Community    schedule 31.03.2009
comment
Однако это специфично для malloc, а как насчет вызова других функций распределения ресурсов? а я и не знал, спасибо - person shodanex; 31.03.2009
comment
Только из-за размещения переменных по этой метрике исходный код был таким же... В любом случае, это просто пример. - person Greg Rogers; 31.03.2009
comment
@Neil: Интересный момент, но я с shodanex - в общем, вам нужна стратегия для обработки других типов освобождения ресурсов. - person j_random_hacker; 31.03.2009
comment
@RA Если мой код специфичен для C99, то и ваш - в чем ваша точка зрения? - person ; 31.03.2009
comment
@j_random - очевидный ответ на этот вопрос состоит в том, чтобы ваши функции, которые выделяют/освобождают другие ресурсы, имели ту же семантику, что и malloc и free. - person ; 31.03.2009
comment
@Neil: бесплатно (0) можно только в C99. - person sharkin; 01.04.2009
comment
@Neil: Достаточно честно, это выглядит довольно удобным шаблоном. Не могли бы вы предложить это в основном посте? Спасибо. +1. - person j_random_hacker; 01.04.2009
comment
@Neil: После некоторого чтения я обнаружил, что вы правы. Извините за это, обычно я знаю лучше, чем полагаться на информацию без источника. - person sharkin; 01.04.2009

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

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

person Alex B    schedule 31.03.2009

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

В вашем последнем примере переменные находятся внутри фигурных скобок (т.е. они существуют только в блоке 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);
}
person Martin Fido    schedule 31.03.2009
comment
+1. Хорошая мысль об игре с переменными, которых еще не существует в разделе очистки. - person j_random_hacker; 31.03.2009
comment
Естественно, это опечатка, и, очевидно, я не пытался ее скомпилировать (указатели в секции выхода были бы непригодны). Очевидно, что такие детали на самом деле не являются сутью рассматриваемого вопроса. - person sharkin; 01.04.2009
comment
@RA: Нет, они не суть, но они запутывают то, что вы пытаетесь донести. Когда вы публикуете фрагменты кода, важно убедиться, что они компилируются и работают так, как задумано. - person j_random_hacker; 01.04.2009

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

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

person Henk Holterman    schedule 31.03.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! И ясно, что полное истолкование грехов кода, из которого он исходит, заняло бы целый день — их много и они разнообразны.

person Jonathan Leffler    schedule 31.03.2009

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

person shodanex    schedule 31.03.2009

#define malloc_or_die(size) if(malloc(size) == NULL) exit(1)

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

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

person nelix    schedule 23.04.2009
comment
Я хотел бы, чтобы вы могли опубликовать ссылку на дополнительную информацию о вычисляемом переходе. Звучит интересно. -1 за ваше мнение о сбое выделения памяти. - person sharkin; 23.04.2009
comment
От специалиста по поддержке pulseaudio: В современных системах стало совершенно ясно, что для обычного программного обеспечения пользовательского пространства имеет смысл только прерывание malloc() [...] article.gmane.org/gmane.comp.audio.jackit/19998 - person CesarB; 24.11.2009