Как стать автором
Обновить

Комментарии 27

НЛО прилетело и опубликовало эту надпись здесь
Например, во многих проектах есть правило использовать safe_free (это может быть макрос или inline-функция) для проверки указателя на NULL перед освобождением с помощью free (опционально — занулить указатель). Т. е. даже если программисту очевидно, что указатель ненулевой, он всё-равно будет использовать safe_free, потому что так закреплено в правилах этого проекта. Остаётся надеяться, что компилятор выкинет эти проверки. Один из старейших способов сообщить GCC о ненулевом указателе — использовать __attribute__((nonnull (...)). Насколько я понимаю, теперь этот атрибут действует не только вглубь функций, но и наружу.
Именно для free() эта проверка не нужна, так как он по стандарту должен игнорировать нулевое значение. Если падает — то это уже проблема реализации, которая положила на стандарты, так что приходится так костылить.

А safe_free скорее всего определяют так:
#define safe_free(ptr) do { free(ptr); (ptr) = NULL; } while (0)
что не лишено смысла.
Совершенно верно.
Я раз работал в проекте, где на уровне собственного препроцессора (на тикле) определялись многие макросы, в том числе и safe_free. Принцип такой: make собирает сначала исполняемый бинарник, который определяет поведение компилятора/платформы, а затем генерит header для тиклевого препроцессора. Затем прогон на тикле, который делает из headers.tcl.h -> headers.h, и дальше компилит уже проект (использующий headers.h)…

Например (на память):
<% if $std(free_checks_null) { %>
#define safe_free(ptr) { free(ptr); (ptr) = NULL; }
<% } else { %>
#define safe_free(ptr) if ( !(ptr) ) { free(ptr); (ptr) = NULL; } else {}
<% } %>

Вплоть до определения нужен ли такой «цикл» do ... while (0), т.е. ваш пример может выглядеть:
#define safe_free(ptr) <%= [ $need_dummy_do { free(ptr); (ptr) = NULL; } ] %>

Работать было удобно, хоть и компиляторов и платформ был целый зоопарк.
Ну а мы его пользовали не всегда по прямому назначению (например, заместо некоторых макросов или тех же сишных templates, было гораздо удобнее). До сих пор иногда беру для удобства, если пользую c/c++, т.к. много чего делать можно, например пути (сегменты их) — под виндой поворачивает слэш:
char * pdd_path = "<%= [file nativename {conf/data/idx/pdd}] %>";

Если вдруг кому надо — препро-парсер выложу на гитхаб.
естественно очепятался, при !(ptr) тело ложится в else:
#define safe_free(ptr) if ( !(ptr) ) {} else { free(ptr); (ptr) = NULL; }
ну или
#define safe_free(ptr) if ( (ptr) != NULL ) { free(ptr); (ptr) = NULL; } else {}
Так можно сказать практически про любую оптимизацию.

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

Особенно заметно это при каскадном инлайнинге кода через несколько функций.
И вы так реально пишете программный код, как привели в примерах? А потом видимо еще не понимаете, почему же всё падает. Очень всё же интересует, почему сначала делается memove, а только потом проверяется указатель. Надеяться на какое-то определенное поведение сторонней функции изначально глупая затея.

int wtf( int* to, int* from, size_t count ) {
if (from == 0) return 0;
memmove( to, from, count );
return *from;
}

Чем такой код не устраивает?
И вы так реально пишете программный код, как привели в примерах? А потом видимо еще не понимаете, почему же всё падает.
Вы это серьезно? Новая оптимизация может доломать кучу «успешно работавшего» десятилетиями кода в проектах размером в сотни тысяч и миллионы строк.
Описанная выше ситуация может произойти вследствие трансформаций. Все то, что компилятор имеет право делать, он так или иначе будет делать.

В какой-то конкретной ситуации, компилятор может посчитать, что для снижения register pressure имеет смысл поменять порядок блоков кода с A, B, C на A, C, B, если это не повлияет на результат. А влияние на результат он рассчитает исходя из всех доступных соображений, включая и описанное в статье.

В итоге — различное поведение.
Плохо конечно, что легаси ломается, но пользователи undefined behaviour сами себе злобные буратины. В языке явно сказано, что так делать нельзя.
PVS-Studio говорит: V595 The 'from' pointer was utilized before it was verified against nullptr. Check lines: 16, 17. test.cpp 16.

Я просто оставлю это здесь: Примеры V595. Здравствуй новый глючный мир. :)
Выдается для всех примеров в публикации или только для первого?
Для всех (wtf, magic1, magic3).

Анализатор правда молчит про подозрительный magic2. Впрочем, этот код может ведь быть и правильным :). Вдруг printf( «null\n» ); кидает исключение и до memmove() мы не доберёмся.
Выдается на magic3() с «велосипедом» или с memcpy()?
На mymemcpy(), к сожалению, не ругается. Быть может, когда научится.
А что не так в вызове magic3(0, 0, 0) с «велосипедом» внутри?
Мы (пока) не знаем, будет разыменовываться в mymemcpy() указатель или нет.
По стандарту extern «C» функции не могут кидать исключения (чем кстати некоторые компиляторы на некоторых платформах пользуются, отключая генерацию unwind info, после чего отваливается пробрасывание исключений через С-шный код из С++-ных коллбеков, увы)
Пробрасывание исключений через код на C — изначально плохая идея. Даже если предположить, что исключение «просто пролетит», очевидно, что код на C никак не сможет на него отреагировать — в результате код на C, который собирался изменить состояние программы после возврата управления из callback-функции, сделать это не сможет, данные программы могут оказаться в рассогласованном состоянии.
Да, разумеется, в общем случае оно так.
В частном случае это был libpng, где предполагается делать error handling с помощью setjmp, но исключения тоже работают… на некоторых платформах :)
Непонятно, что я сказал такого неугодного, что лепят минусы. Видимо кто-то подумал, что я не уважаю его любимый компилятор. Отнюдь. Виноват кривой код. И его, как я показал, весьма много.
А с -Wall -Wextra никаких варнингов не выдаёт? (мне самому сейчас лень gcc-4.9.0 ставить)
Наслышан о чрезмерно агрессивной оптимизации GCC 4.9. Но в этом конкретном примере, если memmove(0, 0, 0) это неопределённое поведение, то почему вы решили, что программа после такого вызова функции вообще продолжит нормально выполняться и даст корректный результат?
Понятно, что в каких-то конкретных реализациях компилятора это работает (или работало до этого дня), и такими реализациями могут оказаться даже все существующие компиляторы, но по стандарту это стрельба в ногу, хоть и холостыми патронами. GCC 4.9 решил помочь и дал вам боевые.
Нужно добавить, что gcc 4.9.0 смотрит не на «memmove», а на __attribute__((nonnull)).
Мы на этом погорели разок, долго на ассемблерный код пришлось смотреть. Диагностика тут у 4.9.0 просто никакая.
Проверка указателя на ноль вообще неясно, зачем нужна. Если, конечно, нулевое значение не является функциональным и что-то значит само по себе. Ничто не мешает размепить нулевую страницу и ловить такие пойнтеры на page fault.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий