c99 перейти к предыдущей инициализации

Во время отладки сбоя я столкнулся с этой проблемой в каком-то коде:

int func()
{
    char *p1 = malloc(...);
    if (p1 == NULL)
        goto err_exit;

    char *p2 = malloc(...);
    if (p2 == NULL)
        goto err_exit;

    ...

err_exit:
    free(p2);
    free(p1);

    return -1;
}

Проблема возникает при сбое первого malloc. Поскольку мы перескакиваем через инициализацию p2, она содержит случайные данные, и вызов free(p2) может привести к сбою.

Я ожидаю/надеюсь, что это будет обрабатываться так же, как в C++, где компилятор не позволяет переходить через инициализацию.

Мой вопрос: разрешен ли переход через инициализацию стандартом или это ошибка в реализации gcc c99?


person R Samuel Klatchko    schedule 12.05.2010    source источник
comment
Видя, как некоторые программы могут знать, что они делают, перескакивая через инициализацию, я держу пари, что это разрешено, но это вечер перед продолжительными выходными в моем часовом поясе, поэтому я не смотрю. Хотя вопрос отличный.   -  person Pascal Cuoq    schedule 12.05.2010
comment
Интересно - насколько я могу судить с первого взгляда, стандарт C99 ничего не говорит о прошлом объявлении/инициализации goto, за исключением того, что в нем говорится, что вы не можете перейти в область, в которой есть VLA, из-за пределов области. Опять же, может быть, я не везде ищу...   -  person Michael Burr    schedule 12.05.2010
comment
Это очень хороший пример того, почему goto считается вредным. Я не совсем против этого, но это может сделать такие вещи простыми, но менее очевидными.   -  person nategoose    schedule 13.05.2010
comment
@nategoose: к сожалению, обработка ошибок такого рода является одной из областей, в которых goto имеет законное применение (во всяком случае, я так думаю). Жаль, что это все еще довольно подвержено ошибкам даже для этого использования.   -  person Michael Burr    schedule 13.05.2010
comment
@Michael Burr: я не говорил, что у него нет законного использования. Мой кухонный нож имеет законное применение, но я все еще считаю его довольно опасным. Помните об опасностях при использовании ножа, а также переходите к делу. В этом коде было бы легко иметь if(p1=malloc(...)){if(p2=malloc(...)){ return use(p1,p2);} else { free(p1); } } return -1; Не все в одной строке, конечно.   -  person nategoose    schedule 13.05.2010
comment
@nategoose: извините, если мой комментарий показался вам противоречащим - он был задуман как более или менее согласие, но желание, чтобы goto работало лучше для этого (или что-то в этом роде).   -  person Michael Burr    schedule 13.05.2010
comment
@Michael Burr: Мне тоже жаль. Я не воспринял ваш комментарий как негативный и сам не собирался показаться негативным. Поскольку многие люди (теоретически) учатся из того, что здесь размещено, я не хотел, чтобы кто-то понял, что goto по своей сути хорошо или плохо, или даже способ обработки ошибок или нет. Иногда это лучшее средство, иногда худшее, а иногда лучшее, но используемое очень плохо.   -  person nategoose    schedule 13.05.2010


Ответы (7)


Вы можете попросить gcc предупреждать вас, когда вы переходите через определение переменной, используя -Wjump-misses-init, а затем вы можете использовать -Werror (или, точнее, -Werror=jump-misses-init), чтобы заставить пользователей иметь с этим дело. Это предупреждение включено в -Wc++-compat, чтобы разработчики gcc знали, что код ведет себя по-разному в C по сравнению с C++.

Вы также можете немного изменить код:

int func()
{
    char *p1 = malloc(...);
    if (p1 == NULL)
        goto err_exit_1;

    char *p2 = malloc(...);
    if (p2 == NULL)
        goto err_exit_2;

    ...

err_exit_2:
    free(p2);
err_exit_1:
    free(p1);

    return -1;
}

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

person florin    schedule 12.05.2010
comment
На самом деле вы можете сделать лучше - -Werror=jump-misses-init превратит только это предупреждение в ошибку (я думаю, это было доступно с gcc 4.3). - person Cascabel; 12.05.2010
comment
@Jefromi - очень мило, я не знал об этом. - person florin; 12.05.2010
comment
Немного не по теме, но пока я об этом, gcc также понимает -Wno-error=..., так что вы можете превратить все предупреждения в ошибки, а затем исключить определенные, если хотите. - person Cascabel; 12.05.2010
comment
В этом коде есть ошибка. Если какой-либо malloc терпит неудачу, вы все равно пытаетесь free(NULL) по крайней мере один раз. - person nategoose; 13.05.2010
comment
@nategoose - free(NULL) является законным и определяется как недопустимый. - person R Samuel Klatchko; 13.05.2010
comment
@nategoose Хорошо, каждая метка должна быть на одну строку ниже, чем она есть. - person Nick Lewis; 13.05.2010
comment
@nategoose: Он может назвать это неэффективностью, но это не ошибка. free(NULL) это совершенно законно. - person AnT; 13.05.2010

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

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

int main() {
  int n = 5;
  goto label; // <- ERROR: illegal jump
  int a[n];
label:;
}

Другими словами, неправильно говорить, что «прыжок — это просто прыжок в C». Переходы несколько ограничены, когда дело доходит до входа в область переменных, хотя и не так строго, как в C++. Ситуация, которую вы описываете, не является одной из ограниченных.

person AnT    schedule 12.05.2010

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

int func()
{
char *p1 = NULL;    /* So we have a defined value */
char *p2 = NULL;

  p1 = malloc(...);
  if(!p1)
    goto err_exit;

  p2 = malloc(...);
  if(!p2)
    goto err_exit;

  ...

  err_exit:
    free(p2);
    free(p1);

  return -1;
}
person Patrick Schlüter    schedule 13.05.2010
comment
+1 Я предпочитаю только одну метку goto для таких простых случаев, как этот, которые не требуют отдельных меток для «ранних ошибок». - person James Morris; 13.05.2010

Это не ошибка в gcc. Прыжок — это просто прыжок в C. Никакой специальной логики не применяется. Проблема в том, что вы сначала не инициализируете свои указатели на NULL. Если бы вы сделали это, то ваш бесплатный вызов был бы free(NULL), который не рухнул бы. Запустите функцию с char *p1 = NULL, *p2 = NULL; и все будет хорошо.

person charlieb    schedule 12.05.2010
comment
Целесообразнее определять переменные ближе к тому месту, где они используются. Также чище централизовать обработку ошибок в конце функции и просто переходить к ней при ошибке. - person florin; 12.05.2010
comment
В принципе я согласен, и мне нравится ваше решение, но если вы видите всю функцию на одном экране, то то, где вы объявляете переменные, становится спорным. Если вы не видите всю функцию на одном экране, у вас большие проблемы. - person charlieb; 12.05.2010
comment
Да, я согласен, что указатели и все остальные переменные должны быть инициализированы чем-то, прежде чем вы их используете. В примере вы ссылаетесь на неинициализированную переменную. - person Jim Tshr; 12.05.2010
comment
+1; вы можете пометить разделы кода в своем ответе обратными кавычками - person Christoph; 12.05.2010

если я скомпилирую этот код с флагом -O2

gcc -Wall -std=c99 -O2 jump.c

у меня есть предупреждение:

jump.c: In function ‘func’:
jump.c:10: warning: ‘p2’ may be used uninitialised in this function

и никаких предупреждений без оптимизации

person Oleg Razgulyaev    schedule 12.05.2010

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

int func()
{
    char *p1 = malloc(...);
    if (p1 == NULL)
        goto err_exit_p1;

    char *p2 = malloc(...);
    if (p2 == NULL)
        goto err_exit;

    ...

err_exit:
    free(p2);
err_exit_p1:
    free(p1);

    return -1;
}

Это стандартный шаблон — «ранние ошибки» вызывают переход к более поздней части кода выхода из ошибки.

person caf    schedule 12.05.2010
comment
Моя настоящая забота заключается в том, как определить, где такой код мог попасть в нашу кодовую базу. - person R Samuel Klatchko; 13.05.2010
comment
@R Samuel Klatchko: Если вы скомпилируете с -Wuninitialized (для чего требуется -O1 или выше), вы получите предупреждение: foo.c:10: warning: ‘p2’ may be used uninitialized in this function - person caf; 13.05.2010
comment
спасибо за предложение. К сожалению, -Wuninitialized не улавливает этого для меня (мы все еще используем gcc 3.4.5). - person R Samuel Klatchko; 13.05.2010

Использование gotos — не самая разумная идея, и вы только что нашли одну из причин, почему. Вы должны вызывать функцию обработки ошибок для каждой отдельной ошибки.

person Puppy    schedule 12.05.2010
comment
Проблема не связана с goto. Та же проблема может быть воспроизведена с другими типами переходов, такими как оператор switch/case. - person AnT; 12.05.2010
comment
Это просто глупо. goto имеет совершенно полезные приложения, и его использование здесь можно увидеть практически во всех основных проектах C, включая ядро ​​​​Linux. Использование goto для обработки ошибок — хорошо зарекомендовавшее себя использование goto. Прекратите продвигать невежественный и категорический запрет неправильно понятых особенностей языка! - person Chris Lutz; 12.05.2010
comment
Этот ответ просто старый неправильный. Ответ Флорина - стандартная идиома здесь. - person Joshua; 12.05.2010