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

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

Ожидаемая правильная работа программы — это просто один из вариантов неопределённого поведения

А какие бывают варианты неопределенного поведения?
Ой, ну разные. Например, запустится игра:
Ещё один пример неопределенного поведения: курьёз с ANSI-директивой «#pragma». Согласно спецификации языка компиляторам предоставлена полная свобода при обработке этой конструкции. До версии 1.17 компилятор GCC при нахождении в исходном коде этой директивы пытался запустить Emacs с игрой «Ханойские башни».
То есть, неопределенное поведение — это набор вариантов, один из которых — попытка запустить Emacs?

Насколько мне известно, это называется unspecified behaviour.

А неопределенное поведение потому и названо так, что оно не определено. И никаких вариантов нет.
Определение undefined behavior из Стандарта C++03 (1.3.12)
behavior, such as might arise upon use of an erroneous program construct or erroneous data, for which this International Standard imposes no requirements.

[...], на которое этот Стандарт не налагает никаких требований — допускается любое поведение, в том числе и запуск Emacs. В принципе, никто не мешает разработчикам компилятора специально сделать неопределенное поведение разнообразным — например, по пятницам программа будет запускать Emacs, а по остальным — выполнять действие, приводящее к аварийному завершению программы, но пользователи не очень это оценят.
Да, это я и имел ввиду. При этом ни о каких вариантах речи нет. Спасибо за ссылку на пункт стандарта.
В Стандарте действительно не сказано ничего о вариантах неопределенного поведения. В статье, очевидно, речь идет о возможных вариантах работы конкретного результата компиляции программы конкретной версией конкретного компилятора с конкретными настройками. Одни компиляторы вообще неспособны к описанной оптимизации, у других она включается в зависимости от настроек (в достаточно новом gcc она включается автоматически, начиная с уровня -O2). В зависимости от этих факторов и возможны разные варианты работы программы.
Однако, меня смущает то, что, хотя все говорят про неопределённое поведение, нигде нет точного разъяснения на этот счёт.


В стандарте C++03 есть разъяснение. Пункты 1.9/4 и 5.2.5/3.

Это я видел. Было 1.9/4: Certain other operations are described in this International Standard as undefined (for example, the effect of dereferencing the null pointer).
А что с новым то стандартом? Почему они застеснялись прежнего описания и теперь там иной пример 1.9/4: Certain other operations are described in this International Standard as undefined (for example, the effect of attempting to modify a const object).
А как изменение примера могло повлиять на поведение? Пример могли вообще удалить, его присутствие необязательно.

Какого-то нового описания разыменования нулевого указателя не появилось, поэтому по данному вопросу все так же, как и было. Пункт 1.9/4 устанавливает неопределенное поведение.
Но мне так и не понятно, что ответить. :) Мне продолжат писать письма, что в статье ошибка и я/анализатор не прав. :)
Прошу помощи. Кто сможет написать бронебойный текст вида: данный код корректен/не корректен, так как согласно современному стандарту C++11 бла… бла… см. пункты такой и такой. Буду благодарен.
Это не те пункты. В Стандарте C++03 нужное определение дано в 1.3.12
undefined behavior [defns.undefined]
behavior, such as might arise upon use of an erroneous program construct or erroneous data, for which this International Standard imposes no requirements.

imposes no requirements означает «не налагает никаких требований» — допускается любое поведение.
Я думаю, стоит задать вопрос в gcc-help. Там живые разработчики компилятора могут ответить, что они думают на этот счёт.
Прислали ещё следующее мнение:
Я прочитал вашу статью «Размышления над разыменованием нулевого указателя» и предыдущую «PVS-Studio покопался во внутренностях Linux (3.18.1)».

Обнаружил два заблуждения:

· При оптимизации компилятор рассуждает так. Вот здесь указатель разыменовывается: podhd->line6. Ага, программист знает что делает. Значит указатель здесь точно не равен нулю. Отлично, запомним это.

· Любое разыменование нулевого указателя — это неопределённое поведение.


Собака здесь зарыта в том, что разыменование указателя a->b это две последовательных операции:

· Вычисление значения lvalue;

· Использование объекта lvalue.

В случае с NULL ничто не мешает вычислить lvalue, и согласно стандарту его вычислят здесь нет неопределенного поведения. А вот в момент использования объекта lvalue компилятор уже с полным основанием может полагать, что в функцию передан ненулевой указатель, либо порадовать нас неопределенным поведением в противном случае. Если lvalue используется в выражении указателей, то нет основания полагать, что исходный указатель был NULL.

Похоже, что вы просто столкнулись с багом программы PVS-Studio, когда она некорректно выдает предупреждение V595 в случае вычисления lvalue без реального использования объекта.

Хотел уже гуглить, чтобы это написать. Тоже читал, что только использование результата разыменования нулевого указателя влечёт UB. Не помню, приводились ли в том пояснении ссылки на стандарт.
в статье же есть комментарий

Прежде всего вспомним, что сишные компиляторы используются в мире эмбеддеров, там struct и union — весьма частые гости. Конструкция offsetof в указанном виде нормально работает типами struct и union. Если коротко — макрос offsetof успешно выдает смещение элементов структур вида offsetof(POINT,str_my_point.x). Вот пример разжевывающей статьи и ее перевод на русский.

Если эта реализация макроса offsetof вас смущает — покажите как без нее обойтись в случаях со struct'ами и union'ами. Просто чтоб вычисляло смещение и компилировалось без ошибок — как?
Спасибо за ссылку на перевод, полезная информация.
После прочтения вспомнил что несколько лет назад набрел как-то на статью где описывалась очень странная по их мнению поведение gcc и способы обхода этой баги. То есть проблема там была не надуманная а реальная, взятая с какой-то либы. За точность не ручаюсь, но с учетом того что прочитал теперь понял для чего там использовался этот offset, точнее использовался для объяснения проблемы. Итак.

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

typedef struct A_
{
bool t;
long long s;
} A;

Тут имеем структура sizeof(A) == 12. В одной из функции в библиотеке структура инициализировалась последовательно присваивая ее поля. Потом эта другому экземпляру этой структуры присваивалась та которая была уже заполненная. После чего по какой-то логике, зачем и почему не помню и не хочу :), для сравнения структур, точнее идентичности полей, использовалась «memcmp()».
Внимательные сишные эмбедерские комрады сразу заподозрили неладное с этой структурой, и не зря. Для тех кто считает битики и байтики экономя на всем, видят тут что в структуре будет использовано выравнивание из-за «bool».
Написал примерчик, как раз было очень удобно использовать этот «offsetof()»:

GCC 32 — ideone.com/qc3G4B
A struct size: 12 — Paddind A struct: 3 — memcmp res: 1
B struct size: 12 — Paddind B struct: 0 — memcmp res: 0

MSVC 2013 x64:
A struct size: 16 — Paddind A struct: 7 — memcmp res: -1
B struct size: 16 — Paddind B struct: 4 — memcmp res: 0

Но у меня еще остался вопрос если выравнивание идет по 4 байт, в случае как с MSVC, memcmp все равно выдает 0, то есть едентичность, тут я пока не разобрался, специфика реализации? Хотя GCC в этом случае так же вернет 0.

Ну и разрабам PVS. Можете записать в свой «todo list». Баг очень специфический, но это баг из реального кода библиотеки. Навряд такое в ядре найдешь, но кто знает.

А ну и как разрабы те решили проблему? написали макрос по типу "#define our_super__struct_memcmp(a, b)", где сравнивали поля структуры по отдельности.
А вот в случае когда структура копируется в другую посредством простого присваивания, а не инициализации полями, memcmp() возвращает ожидаемый 0.

ideone.com/64eCVm

GCC:
A struct size: 12 — Paddind A struct: 3 — memcmp res: 0
B struct size: 12 — Paddind B struct: 0 — memcmp res: 0

MSVC 2013 x64:
A struct size: 16 — Paddind A struct: 7 — memcmp res: 0
B struct size: 16 — Paddind B struct: 4 — memcmp res: 0

Вот так.
Ну и про сам «offset». MSVC версия прям вообще со всеми предосторожностями если используется С++…

#ifdef __cplusplus

#ifdef _WIN64
#define offsetof(s,m) (size_t)( (ptrdiff_t)&reinterpret_cast((((s *)0)->m)) )
#else /* _WIN64 */
#define offsetof(s,m) (size_t)&reinterpret_cast((((s *)0)->m))
#endif /* _WIN64 */

#else /* __cplusplus */

#ifdef _WIN64
#define offsetof(s,m) (size_t)( (ptrdiff_t)&(((s *)0)->m) )
#else /* _WIN64 */
#define offsetof(s,m) (size_t)&(((s *)0)->m)
#endif /* _WIN64 */

#endif /* __cplusplus */
На всякий случай добавлю замечание.

Тут проблему можно еще решить обнулением участка памяти созданного объекта структуры, в простонародье:
memset(&r1, 0, sizeof(A));
memset(&r2, 0, sizeof(A));

ideone.com/OaipoS
Что за жесть? Почему бы просто не проинициализировать при создании:

structTypeName r1 = { 0 };
structTypeName r2 = { 0 };
memset() гарантировано зануляет память структуры включая неиспользуемое место из-за выравнивания(padding). А вот скобочки до принятия стандарта С11(6.7.9-10) НЕ гарантировало обнуление padding и тогда memcmp() могло работать не как ожидалось, а в случае если структуру отправлять по сети или запиcывать в файл — возможно будет включен мусор.

Вот поэтому во всех, почти во всех, сишных библиотеках, ядрах..., структуры заполняют нулями с помощью memset().
Зарегистрируйтесь на Хабре, чтобы оставить комментарий