Pull to refresh

Comments 193

А что насчёт микроконтроллеров? У них данные реально могут находиться памяти с адресом 0. То есть ноль разыменовывать как бы можно и даже нужно. Само собой, компилятор там свой и наверняка допускающий это, но в статье ведь речь про стандарт языка, а не про компилятор? Как быть с этой ситуацией?
Я понимаю в данном случае
NULL != ZERO
но я могу быть не прав
Я не думаю, что есть смысл говорить о микроконтроллерах. Это особый мир, где вынужденно допускаются вольности.
Это беззаконная постапокалиптическая пустошь, в которой нет ничего, коме голого железа…
может уважаемые люди помогут переписать offsetof так чтобы и по стандарту и без разыменования nullPtr?
Ну или пусть хоть озадачатся.
а то осталось какое-то чувство незавершенности…
Это ненужно. Вы просто должны использовать offsetof(). А как он устроен, через __builtin_offsetof() или иные расширения, не важно. Это уже нюансы реализации.
GCC точно не может. Ради интереса глянул на реализацию __builtin_offset_of. А там:

/* Build the (type *)null that begins the traditional offsetof macro. */
expr = build_static_cast (build_pointer_type (type), null_pointer_node, tf_warning_or_error);
/* Parse the offsetof-member-designator. We begin as if we saw «expr->». */
expr = cp_parser_postfix_dot_deref_expression (parser, CPP_DEREF, expr, true, &dummy, token->location);
Ну и отлично.
Как насчет операционных систем?

Допустим, у нас такая ОС и виртуальное адресное пространство так смапировано, что нулевой адрес всегда указывает на валидный участок памяти, и там находится одна из управляющих структур, которых, например, десяток подряд. Адрес 0 — вполне себе валидный, проверять !=NULL нет смысла вообще. Как тогда?
В плюсах есть такая интересная штука — указатель-на-член данных. По сути, это тот же offset от начала структуры, только типизированный. Но стандарт требует, чтобы этому указателю можно было присвоить нулевое значение (nullptr), которое значит, что он никуда не указывает. Как же быть, ведь смещение на 0 относительно начала структуры — это вполне легитимная вещь? Просто берут и хранят не само смещение, а смещение + 1 (то есть смещение в 0 становится единицей и т.д.), мне кажется, и в вашем случае подобным трюком можно воспользоваться.
Трюк-то возможен, но это костыль. Зачем в таком полу-низкоуровневом языке, как Си, такое ограничение…
А никак.
Такой код под микроконтроллер вы не сможете скомпилировать и запустить, скажем, на i386.
Точнее, сможете, но поведение будет не таким, как на микроконтроллере (вызовет исключение; выдаст ошибку; пройдёт незамеченным; вызовет Ктулху...). В общем — будет НЕОПРЕДЕЛЁННЫМ, о чём и говорит стандарт.
i386 тут ни при чём. Можно и скомпилировать и запустить. Поведение зависит от ОС. По конвенции большинство ОС не мапят первую страницу виртуальной памяти с нулевым адресом как раз для того, чтобы отлавливать обращение по нулевому указателю. Но если очень надо, в какой-нибудь своей ОС можно замапить эту страницу и нулевой адрес будет вполне валидным.
Да, кстати. В KolibriOS, например, указатель на 0 — это указатель на начало адресного пространства приложения. Через него можно получить указатели на cwd и argv.
В этом и есть суть неопределённого поведения. Оно определяется не языком, а некими внешними условиями (железом, ОС и т.д.). И если результат 1+1 можно определить в стандарте независимо от ОС, то разыменование нулевого указателя выходит за эти рамки.
В стандарте же есть замечание (8.3.2.4 [Note]):
in particular, a null reference cannot exist in a well-defined program, because the only way to create such a reference would be to bind it to the “object” obtained by dereferencing a null pointer, which causes undefined behavior
которое недвусмысленно указывает на то, что разыменование нулевого указателя — это UB.
in particular, a null reference cannot exist in a well-defined program

Почему так?
Ну там же дальше написано) Потому, что единственный способ сделать это — инициализировав ссылку через разыменование нулевого указателя, но разыменование нулевого указателя — это UB, а значит такая программа, с точки зрения стандарта, ill-formed.

Вообще, я затрудняюсь предположить, зачем кому-то может понадобиться делать нулевую ссылку.
Опять 25.
«UB потому что мы разыменовываем нулевой указатель»

Я и спрашиваю — почему разыменовывание нулевого — некорректно.

Вообще, я затрудняюсь предположить, зачем кому-то может понадобиться делать нулевую ссылку.

Писал уже выше, и другие приводили примеры — в других, не мейнстримовых ОС нулевой адрес вполне может быть корректным в виртуальном АП.
Я и спрашиваю — почему разыменовывание нулевого — некорректно.
Так говорит стандарт. Что стандарт — плохой, негодный, это уже другой вопрос.
Я и обращаюсь к коллегам — почему, мол, как вы думаете, такое написано в стандарте.

Однако ниже уже подсказали, что NULL — необязательно 0.
Добавлю свои 5 копеек:

В случае С++ можно определить для класса свои операторы operator& и operator->. Таким образом, выражение &P->m_foo может скрывать за собой вызов пользовательских методов. Соответственно возможно что компилятор не сможет соптимизировать пользовательские операторы должным образом и разименует нулевой указатель — ещё один аргумент чтобы не писать &P->m_foo.

Именно поэтому зачастую компиляторы реализуют offsetof() используя свои собственные расширения:
#define offsetof(st, m) __builtin_offsetof(st, m)
UFO just landed and posted this here
Так что проверка на NULL — не панацея. Нужно аккуратней писать программы.

Это, в том числе, ответ и для antoshkka: и по возможности вообще не использовать сырые указатели и ручное управление памятью ибо C++11 уже очень многое, буквально всё, позволяет писать с использованием smart_pointer и вообще не думать о подобных вещах, а главное и производительность в итоге не страдает ибо на уровне ассемблера всё разворачивается в аналогичный код всё с теми же проверками без какого либо оверхеда, но это уже делается автоматически и не требует пристального внимания.
+1 за умные указатели!

К несчастью, даже новые учебники по С++ в первую очередь учать сырым указателям. Так что в ближайшем будущем не стоит ожидать большого количества проектов без сырых указателей. Да и ряд вещей требуют обычных указателей (различные интрузивные контейнеры и т.п.).

При желании можно прострелить себе ногу и с умными указателями:
* умные указатели тоже могут держать NULL (так что от проверок не избавиться)
* умным указателям так же можно скормить указатель, ни на что не указывающий
Учить сырые указатели нужно ибо это действительно основы без которых никуда и пробовать их использовать на простых примерах тоже, знаний это даст больше о реально происходящих в машине вещах. С сожалением соглашусь, что о существовании умных указателей, особенно в книжках для начинающих почти никто не говорит. Это действительно проблема поскольку люди приучаются писать на C++11 используя методики C++03. К сожалению, также отмечу, что это не только умных указателей касается, а ещё и Boost, который фактически является расширением STL и решает большинство не решённых из коробки задач.

Вот про применение в точку — их нужно использовать только там, где без них никак. Конечно, умные указатели тоже позволяют прострелить себе ногу, но ведь в большинстве случаев можно не только их применить, но и от ручных вызовов new/delete отказаться. Да и как показывает статистика ошибки встречаются чаще как раз в самом простом коде, где можно спокойно не использовать низкоуровневое управление памятью.
Очевидно, что есть некая «физика», которая диктует то, как в компиляторах языка реализуются те или иные его фичи.
Я, как embedded developer, имел дело с большим числом разных архитектур и компиляторов, и ни разу не сталкивался с какими-то особенностями, из-за которых трюки типа макроса offsetof не будут работать.

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

Мораль — по бумажкам, т. е. формально, это запрещено, но на практике я не знаю ни одного пешеходного перехода, для которого такие фокусы могут привести хоть к каким-то реальным проблемам.
Про микроконтроллеры тут уже сказали. Можно усложнить задачу, порекомендовав запустить ОС без MMU. Например, тот же ucLinux.

Я проигнорирую доказательство со ссылкой на википедию — ну как-то не серьезно ;)

Касаемо С99. Рекомендую все-же прочитать оригинал. Shall/must — это бич всех переводов. Shall — скорее имеет рекомендательный характер. Для «должен» там было бы «must».
Shall/must — это бич всех переводов. Shall — скорее имеет рекомендательный характер. Для «должен» там было бы «must».
Ага, «You shall not pass» тоже рекомендация. Не путайте «shall» и «should».

RFC2119.
Да-да, всё так и есть. Thou shalt not kill — тебе не рекомендуется убивать.
В последние годы стала складываться тенденция трактовать стандарты C/C++ строго формально в части неопределенного поведения. Это приводит к тому, что писать на C становится едва не опаснее, чем на ассемблере.

Хоть формально законы не нарушаются, но цель существования языка высокого уровня — повышение предсказуемости, понятности и прозрачности программ — подрывается.
Возможно. Но только следует понимать и помнить, что делается это не из-за вредности или садизма. Эта плата за крайнюю эффективность языка. Например, раз «this» не может быть равен NULL, то можно выбросить проверку if (this == 0) и ускорить программу. Раз после memset() массив не используется, можно выбросить этот memset(), хотя он планировался для обнуления секретного пароля. И так далее. Ничего личного, просто бизнес оптимизация.
делается это не из-за вредности или садизма. Эта плата за крайнюю эффективность языка

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

В связи с этим, почему бы программы сразу не писать на ассемблере? И тут все скажут: «Ну как же, ассемблер сильно наказывает за ошибки, с ним надо работать предельно аккуратно, а это — лишние трудозатраты». Хорошо, но если ради повышения эффективности языка C (C++) программирование на нем превращают в прогулку по минному полю, ничуть не более безопасную, чем работу с ассемблером — то зачем он такой нужен? Лучше уж сразу на ассемблере.
— Доктор, скальпель крайне эффективен, остальные инструменты могут лишь приближаться к его эффективности, но никогда ее не превзойдут!
— Пациент, у вас аллергия…
— … но Доктор! Вы не хотите работать скальпелем потому что он сильно наказывает за ошибки, с ним надо работать предельно аккуратно, а это — лишние трудозатраты?


Мораль: Под каждую задачу свои инструменты.
Аналогия неверная. Эмоций много, вы попытались занять очень выгодную позицию «доктора», который лучше знает, каким средством надо пользоваться, но все это не по существу вопроса.

Язык Си пытается соперничать с ассемблером по скорости (точнее, приблизиться к ней) и поэтому в данном контексте должен рассматриваться как инструмент для решения тех же задач, что и ассемблер. В терминах вашей аналогии — это может быть два врачебных инструмента разных типов, но оба предназначены для разрезания мягких тканей, а не «лечения аллергии».

Если сравнить ассемблер с обычным скальпелем (который «сильно наказывает за ошибки»), а С — с каким-нибудь высокотехнологичным инструментом, типа лазера, который требует меньших навыков и тренировки хирурга для получения тех же результатов — то получается такая картина. Новый инструмент более легкий в обращении, но режет хуже. Фирма-производитель совершенствует новый инструмент, но по мере этого совершенствования возрастают требования к внимательности и навыкам хирурга, что делает новый инструмент по сложности обращения сравнимым с обычным скальпелем.

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

Человек конечно может выполнить роль аллокатора регистров и перетряхивать весь набор инструкций при малейшем изменении, но на значительном участке кода он выполнит эту задачу чуть менее чем никак.
UFO just landed and posted this here
Примеры в самую точку. Все вокруг оптимизации.
UFO just landed and posted this here
Например, раз указатель разыменовывался, он не может быть равен 0. Следовательно можно убрать лишнюю проверку. Как раз эти рассуждения я приводил в самой первой статье.
Например, раз указатель разыменовывался, он не может быть равен 0.

Это почему?
UFO just landed and posted this here
значит мы имеем дело с неопределённым поведением

Это кольцевое доказательство, которое ссылается само на себя. Вы запутались.

Разговор идет так: разыменовывание нуля — это UB. Вопрос — почему? Ответ — «раз указатель разыменовывался, он не может быть равен 0». Вопрос — почему? Откуда, мол, такая уверенность, что адрес 0 — невалидный?
И ваш ответ «Потому что если он был 0 и был разыменован, значит мы имеем дело с неопределённым поведением.»

Получилась чушь.
UFO just landed and posted this here
Стандарт не обсуждается, он дан нам свыше.

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

Хоть формально это не противоречит стандарту, но практическое применение такой логики способствует исключительно появлению в программе труднообнаружимых ошибок. И поэтому — наносит вред. Следует задуматься, что нужнее — точное следование «букве закона», или же облегчение работы программиста, помощь ему в написании качественных программ?
UFO just landed and posted this here
Обсуждать стандарт можно, но большого смысла в этом нет (по крайней мере если это не обсуждение с людьми, которые участвуют в составлении стандарта)

Не соглашусь. Даже наше с вами «кухонное» обсуждение стандарта может иметь далеко идущие последствия. Кто-нибудь из участников или свидетелей обсуждения составит на этот счет свое мнение, которое распространит по своим знакомым и т.д. Как известно, всех людей в мире разделяет 5-6 рукопожатий, и, если вы хорошо аргументируете свою позицию — то она имеет шанс дойти до разработчиков стандарта.

Особо активные желающие могут прямо связаться с кем-нибудь из комитета и изложить им свое мнение. Так что стандарт — это не незыблемая твердь. Если на то есть основания — его можно и нужно менять.
По-моему тут никаких вопросов с интерпретацией стандарта нет

Вопросы с интерпретацией стандарта есть. В самом стандарте не определено словосочетание «undefined behavior». Оно просто используется в нем. Это уже другие люди, и не в официальных документах, придали ему смысл, что: «может произойти абсолютно все, что угодно, включая самоуничтожение компьютера и запуск ядерных ракет».

Это была хорошая гипербола для мотивации для программистов избегать возникновения случаев UB в программе. Но следует задуматься, с какой целью эти ситуации были введены в стандарт вообще. Была ли это коварно расставленная сеть мин и капканов с целью создать программистам трудности? Или это был просто способ не слишком ограничивать рамки реализаций языка для разнородных компьютерных систем? Если второе — тогда уместно ставить вопрос о разумных границах поведения программы, когда оно стандартом не определено. Допущение абсолютно любых последствий, как мы видим в последние годы — способствует не оптимизации программ, а исключительно снижению их надежности. То есть приносит больше вреда, чем пользы.
способствует более быстрым программам. Источников мёртвого кода может быть множество

С моей точки зрения мертвый код — это не самая главная причина снижения быстродействия программ. Даже в рамках прямой трансляции кода (без удаления мертвого) компиляторам еще есть куда развиваться. Например, я недавно делал сложение многоразрядных чисел и смотрел компилированный код. Компилятор не воспользовался флагом переноса процессора, т.е. не использовал все его возможности. Аналогичная программа на ассемблере была бы быстрее. Вот куда можно направлять усилия. А не на удалении «мертвого» кода очистки памяти от паролей и тому подобных вещах.
UFO just landed and posted this here
Любые границы сильно снизят возможности компиляторов по оптимизации.

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

Я не против выбрасывания мертвого кода. Но против того, чтобы компилятор выбрасывал живой код, а именно это происходит, когда, например, «оптимизируются» вызовы очистки памяти от паролей при выходе из функций. Или при «оптимизации», основанной на обнаружении UB.

«Сестра, а может в реанимацию? — Доктор сказал „в морг — значит в морг!“ ©
Кроме того мёртвый код встречается часто.

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

На x86 не сравнивал, сравнивал на более простых процессорах. Там явно быстрее. В разы. А что, вы сомневаетесь, что и на x86 будет быстрее, если из внутреннего цикла удаляются команды? Мне провести специальный тест для вас?
А сложение многоразрядных цифр обычно пишется в библиотеке (типа GMP) один раз, оптимизируется, при желании хоть на ассемблере и используется другими.

А что мне делать, если я разрабатываю такую библиотеку? Пусть даже один раз. Ведь язык Си вроде бы ставит приоритетом скорость. А вы уже предлагаете переходить на ассемблер.
Вопросы с интерпретацией стандарта есть. В самом стандарте не определено словосочетание «undefined behavior». Оно просто используется в нем.

Вы категорически не правы. Словосочетание Undefined behavior определено на странице 2 стандарта, в разделе «Определения»:
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. Undefined behavior may also be expected when this
International Standard omits the description of any explicit definition of behavior. [Note: permissible unde-
fined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during
translation or program execution in a documented manner characteristic of the environment (with or with-
out the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a
diagnostic message). Many erroneous program constructs do not engender undefined behavior; they required to be diagnosed. ]are
Нет.


Да. Вы говорите «это UB потому что разыменовывание нулевого указателя некорректно».

Я спрашиваю «почему некорректно» и вы отвечаете «потому что это UB». Ок, я спрошу еще раз — а почему разыменовывание нулевого указателя это UB?

Неверно. Разыменование нуля это UB потому что так написано в стандарте. Стандарт не обсуждается, он дан нам свыше.


Я как раз о стандарте, о мотивации данного раздела стандарта и спрашивал, если что.
UFO just landed and posted this here
Спасибо за совет, но я спрашивал со-хабровцев о том, что они думают по поводу того, почему стандарт таков.
Потому что было необходимо выделить специальное значение указателя, обозначающее «там ничего нет». А раз ничего нет, то пытаться это разыменовать не имеет никакого смысла.
Опять то же самое по кругу.

«специальное значение указателя» — это замечательно. Но, допустим, нам необходим доступ к структуре, лежащей по адресу 0. В этом случае «специальное значние указателя», очевидно, необходимо переопределить. Но это невозможно. Это и странно.
UFO just landed and posted this here
Но по стандарту (и ради совместимости) константный ноль (число) обязан конвертироваться в NULL.
Верно, и это уже давно выяснили. Только вот жаль нельзя переопределить NULL на, скажем, x86.
А Вам переопределять и не нужно. Ваш компилятор знает, под какую архитектуру + ОС он компилирует, отсюда, ему виднее, какие адреса можно использовать как заведомо невалидные. Например, в Windows в подсистеме виртуальной памяти хоть для x86, хоть для IA-64 по нулевому адресу гарантированно нельзя разместить объект. Компилятор это знает и использует в этом случае 0 для физического представления null указателя.
Повторюсь еще раз насчет макроса NULL. В C++ NULL можно определить только константным целочисленным нулем. Попробуйте сделать редефайн #define NULL 1. В строчке кода int* p = NULL получите ошибку компиляции. Не кастуются в C++ целые числа к указателям, исключение только для константного нуля интегрального типа. Это правила языка. Кстати, Страуструп против использования макроса NULL в C++, говорит, выбросите его и используйте 0 (хотя, для меня NULL удобнее).
Например, в Windows в подсистеме виртуальной памяти хоть для x86, хоть для IA-64 по нулевому адресу гарантированно нельзя разместить объект.

А толку? Допустим, я компилирую PE / elf модуль под другую ОС (кто сказал, что PE только для MS?), где адрес 0 — корректный. И, допустим, PE заголовок (MZ точнее) аккурат в virtual address space'е процесса по адресу 0 маппируется. А NULL все равно будет численно равен 0. А там, по адресу 0, лежит нужная структура. Ну вот, синтетический случай такой.

PS C++ в данном контексте не интересен.
Допустим, я компилирую PE / elf модуль под другую ОС (кто сказал, что PE только для MS?), где адрес 0 — корректный.

Вы же указываете компилятору целевую ОС. Ну вот, компилятор знает, что в целевой ОС адрес 0 корректный, значит для null-представления он выберет что-то более подходящее.
Случай действительно синтетический. Если бы возникала такая потребность, то компиляторы реализовали бы опции переопределения null-значения.
Это мои размышления, я не претендую на истину :)
Вовсе необязательно. Мы указываем формат файла. Случай не совсем синтетический, выше уже упоминали некоторые ОС. Т.е. я его, конечно, выдумал, чтобы не было споров о частностях, но это реальный сценарий.
И адрес для NULL задан в stdef путем определения #define NULL ((void*)0).
И было бы логично, если бы мы моглм поменять этот дефайн.

Ан нет.
Когда вы присваиваете указателю число 0 — то это не значит, что численное представление указателя (т.е. адрес, на который он указывает) будет равно нулю. Компилятор, при присваивании числа 0 указателю, может записать в его представление любое выбранное им значение.

Вообще в рамках стандарта C вы не можете знать, чему равен фактический адрес указателя. Преобразование указателей в целые и обратно — это непрозрачная операция, она может заменять некий ненулевой адрес null-pointer на число 0 и обратно. К тому же, не гарантируется, что существует такой целочисленный тип, к которому можно обратимо привести указатель.

Если же вы попытаетесь получить доступ к представлению указателя в памяти — то там может быть все, что угодно, там могут быть неиспользуемые битовые поля, заполненные мусором, и никто тем более не гарантирует, что представление null pointer будет нулевым.
Секундочку, я могу согласится, что «присвоение указателю NULL не означает присвоение ему 0», но остальное — спорно. Т.к. повсеместно выполняются прямые арифметические действия с указателями через приведение к целочисленному. Вы не будете так добры и сошлетесь на конкретные пункты стандарта?
повсеместно выполняются прямые арифметические действия с указателями через приведение к целочисленному

Думаю, что это недопустимая практика, фактически, являющаяся неопределенным поведением.

Сошлюсь на стандарт C99.

6.3.2.3., абзацы 3-6 задают правила преобразования указателей в целые и обратно. В частности:
«3 An integer constant expression with the value 0, or such an expression cast to type
void *, is called a null pointer constant.55) If a null pointer constant is converted to a
pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal
to a pointer to any object or function.»
Уже в стандарте C99 присвоение нуля указателю равносильно присвоению ему макроса NULL.

Абзац 6: «Any pointer type may be converted to an integer type. Except as previously specified, the
result is implementation-defined. If the result cannot be represented in the integer type,
the behavior is undefined.
The result need not be in the range of values of any integer
type.
»
Отсюда следует, что преобразование указателя в целое число может быть необратимым. В 7.8.1.4 заданы два типа intptr_t и uintptr_t, но их может не быть в системе («These types are optional»). Для других целых типов обратимость преобразования указателя не гарантируется и подавно.

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

Раздел 6.3.2.3. абзац 5:
«An integer may be converted to any pointer type. Except as previously specified, the
result is implementation-defined, might not be correctly aligned, might not point to an
entity of the referenced type, and might be a trap representation.»
Отсюда следует, что не любое целое число может быть преобразовано в валидный указатель (разыменовывание которого не приведет к UB). Фактически, если вы собираетесь разыменовывать указатель, полученный из числа — то вы должны гарантировать, что ваше целое число будет преобразовано в валидный указатель.
Единственный способ сделать это — получить число путем обратного преобразования заведомо валидного указателя в целое (7.8.1.14, intptr_t).

Любая арифметика над указателями должна производиться над указателями, а не результатами их преобразований в целые числа. Но даже тут стандартом установлено множество ограничений (6.5.6 абзацы 2, 3, 7, 8, 9).

Представление указателей в памяти стандартом не задано (6.2.6.1 абзац 1). Вполне соответствует стандарту C система, в которой указатель не является адресом в линейном адресном пространстве, а состоит из нескольких битовых полей. Некоторые биты представления указателя могут не использоваться, и в них может находиться мусор, по аналогии с типами, для которых представление задано (6.2.6.1 абзац 6).

При сравнении указателей мусорные поля не сравниваются, поэтому могут существовать два и более представления одинакового указателя, такие, что если имеется void* p1, void* p2, может быть p1==p2, но memcmp(&p1,&p2,sizeof(void*))!=0.

Могут существовать два и более представления null pointer. При сравнении как указатели они будут равными, но при сравнении представлений через memcmp результат может быть «не равно» (6.2.6.1. абзац 8, сноска 43).

Поскольку результат преобразования указателя в целое число оставлен целиком на усмотрение разработчика компилятора — то возможна ситуация, когда два null pointers, имеющих разное представление за счет различий в мусорных полях, будут преобразованы в различные целые числа. Обратное преобразование приведет к null pointers, которые гарантированно будут равны между собой, но целые числа, полученные из указателей, могут быть не равны между собой и не равны нулю.

Даже если указатель представлен в виде адреса в линейном адресном пространстве — то возможна реализация, где при преобразовании целого в указатель из этого числа вычитается 1, а при обратном преобразовании — прибавляется. Тогда, если вы присваиваете 0 указателю — то получится адрес 0xFFFFFFFF, а при обратном преобразовании вы получите число 0. При наличии мусорных полей возможны варианты. Например, если мусорными являются старшие 8 бит — то при присвоении 0 указателю вы можете получить адрес 0xAAFFFFFF, при присвоении 0 другому указателю получите адрес 0x55FFFFFF. Обратные преобразования в числа дадут 0xAB000000 и 0x56000000 соответственно. Эти числа не равны нулю и не равны между собой, но если вы снова преобразуете их в указатели — то получите адреса, у которых младшие 24 бита равны 1, и сравнение таких указателей между собой снова даст положительный результат.

Описанный выше сценарий позволяет иметь в системе с 24-битным адресным пространством память по адресу 0x000000, и по этому же адресу может размещаться объект, на него может ссылаться валидный указатель, и все это будет соответствовать стандарту C.
UFO just landed and posted this here
> Например, раз «this» не может быть равен NULL, то можно выбросить проверку if (this == 0) и ускорить программу.

На самом деле нет. Когда я первый раз в MFC-шном коде увидел if (this == NULL)… я очень испугался.
PS: msdn.microsoft.com/en-us/library/d64ehwhz.aspx
Это эксплуатация (exploit :) ) особенностей MSVC и особенностей конкретной библиотеки MFC.
Там известно, что компилятор не срезает все углы, т.е. для него if(this) не тождественно if(true).

Также там известно, что там не будет сдвига базы, поскольку единственный источник нулевых указателей — это CWnd* CWnd::FromHandle(HWND), HANDLE CWnd::GetSafeHwnd() и т.п.
Хотя накренить систему, сделав класс-наследник с заведомо сдвинутой базой, и вызвав именно у него GetSafeHwnd, — легко и просто.
Дополню nickolaym: обратите внимание, что из описания на MSDN вообще не следует, что эта функция как то разрешает сравнивать this с нулём именно разработчику ПО и там лишь говорится, что GetSafeHwnd предназначена как раз для замены такой проверки. На практике это означает, что подобное описание для любого API описывает лишь интерфейс, но никак не реализацию и завтра в глубине this != nullptr может быть авторами API заменено, например, для какой то архитектуры на проверку специального бита в адресе указателя или поля в классе (структуре), вроде, isSet, isValid или вообще на что угодно. Как правильно было сказано, в реализации MFC используется именно эта проверка потому что это обязательная и неизменная из-за необходимости обратной совместимости особенность реализации самого MFC.
Да, да, да и ещё раз да! И ведь при этом ещё те, кто для безопасного затирания данных использует memset и матерят компилятор за «кривую оптимизацию» сами себе враги не только из-за стандарта, а потому что каждая ОС имеет в своей API специальную функцию, например, для Windows это SecureZeroMemory, которая специально создана для этих целей и применение которой прекрасно описано как в документации, так и в рекомендациях для разработчиков.
Ваш комментарий имеет право на жизнь, но вот только но не в данном случае:

P->m_foo — это разыменоввывание. И именно как разыменовывание оно и выглядит на высоком уровне. Однако, вы знаете как на низком уровне &P->m_foo выглядит для компилятора, и понимаете что скорее всего он это соптимизирует и ничего разименоввывать не станет.

Хоть формально законы не нарушаются

Вы как раз нарушили формальные законы высокоуровнего языка, применив знание низкоуровневых вещей.
Да, я нарушил. Да, компилятор формально не нарушал. Ну и что? Для чего вообще нужен язык программирования? Чтобы повесить его в рамку и любоваться? Или чтобы писать на нем программы?

И если писать на нем программы, то я имею полное моральное право высказать мнение о тех или иных аспектах самого языка или направления его развития за последнее время.

Для чего, по-вашему, создавался язык Си? Для чего он, по-вашему, используется? Для чего он должен будет использоваться в будущем, после того, как будет развит в соответствии с принятыми на сегодняшний день тенденциями?
Язык Си используется и будет использоваться для написания UNIX-подобных ОС и базовых программ под них. Он создавался для этой задачи и он для этой задачи (относительно) подходит. А все другие применения — на страх и риск автора.
Т.е. вы утверждаете, что язык Си — не универсальный язык программирования? Мило.
Язык Си попал в лапы к формалистам. Так что не удивляйтесь, если со временем они его так усовершенствуют, что кроме «UNIX-подобных ОС и базовых программ под них» на нем станет невозможно писать что-либо еще.
Нет, не универсальный. Он применим только для разработки портируемых системных приложений, а то, что его используют где-то еще, вообще говоря, объясняется отсутствием вменяемой альтернативы для системного программирования.
Он применим только для разработки портируемых системных приложений

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

Более того, если добавлять в язык Си случаи неочевидной, даже где-то замаскированной, неправильной работы программ — то это сделает его малопригодным и для цели написания UNIX и базового ПО под нее. В наши дни критические уязвимости в системном ПО имеют очень большое значение. И когда компилятор способствует проявлению опасных свойств языка — то он способствует росту количества и опасности этих уязвимостей, затрудняет их обнаружение. Учитывая это, скоро на Си станет слишком опасным писать ОС.
Секундочку.
Так утверждение стандарта, который говорит «ну, раз в основном у нас по адресу 0 обратится нельзя, то примем это как UB» — это и есть платформозависимость.
Нет, это требование платформонезависимости со стороны программы. Т. к. на одной архитектуре нельзя, на другой можно, на третьей такое обращение приведет к ресету всей системы. А корректная программа на C должна работать на всех трех.
Вы знаете, язык Си уже давно используется для многих других целей, кроме написания UNIX-подобных ОС и базовых прикладных программ под них. Например, на Си написана ОС Windows. И огромное количество прикладного и встраиваемого ПО. Но если закрыть на все это глаза и смотреть в одну точку: «язык для UNIX, все остальные идут лесом» — то может получиться, что небольшая группа догматиков начинает «совершенствовать» язык, а на самом деле — делать его непригодным в тех областях, где он еще совсем недавно успешно применялся.

Но даже и в контексте UNIX. Если бы не было Си — на чем можно было бы писать ОС? Есть ассемблер. Есть Паскаль. Есть и более старая экзотика, типа каких-нибудь Алголов. Чем же Си лучше них, почему он, по-вашему, на сегодняшний день является главным выбором языка для написания ядра ОС?
UFO just landed and posted this here
Это не определяющие факторы. Паскаль и Алгол тоже просты. Где-то даже проще, чем Си. И компиляторы для них появились до появления Си-компиляторов. Не говоря уже об ассемблере. Он еще проще и легче в компиляции.
Есть ассемблер
Он сложен и непортируем.
Есть Паскаль
Он требует какого-никакого рантайма. Но вообще — неплохой был бы вариант.
почему он, по-вашему, на сегодняшний день является главным выбором языка для написания ядра ОС
Традиция. И новых низкоуровневых языков в девяностых-нулевых не было.
Есть ассемблер

Он сложен и непортируем.

Чем же он сложен? Набор команд, как правило, очень простой и ограниченный.

Что до портируемости Си — то можно встретить мнения, что этот язык на самом деле непортируемый. Особенно это проявлялось в его первоначальных редакциях. До того, как были введены типы вроде uint32_t — было невозможно даже предсказать, каким будет размер стандартных целых типов на другой платформе.

Типичная задача для системного ПО — запись информации в двоичный файл, а потом чтение из него. Допустим, в вашем файле хранятся 32-битные целые. Какой тип языка Си вы будете использовать для их представления в программе? Типы int и short не подходят — они могут быть 16-битными. Подходит только тип long. Но что если на какой-то платформе long имеет 64 бита? Тогда, если скомпилировать программу под эту платформу, может произойти следующее: 1) файлы поменяют свой формат, и на новой платформе невозможно будет читать файлы, записанные на старой, и наоборот; 2) из-за изменения размера long файлы вообще будут записываться с ошибками формата, т.е. данные в них окажутся испорченными.
Есть Паскаль

Он требует какого-никакого рантайма. Но вообще — неплохой был бы вариант.

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

Если же накладывать ограничения на пользование функциями стандартной библиотеки — то это уже не совсем Си получается. Уже нестандартный язык. Но так же само можно и в Паскале наложить ограничения на пользование возможностями языка (типа запретить команды writeln) — и в рантайме отпадет необходимость.

Я не ратую за Паскаль на самом деле. Последний раз брал его в руки лет 17 назад, а сейчас Си — мой основной рабочий инструмент. Тем не менее хочется понять, почему вы считаете, что Си подходит для системного программирования лучше, чем Паскаль?
Традиция. И новых низкоуровневых языков в девяностых-нулевых не было.

Это веские причины. Но смотрите, что получается. Почему не появлялось новых языков? Почему сложилась традиция использовать Си? Потому что в том виде, в котором существовал этот язык, он всех устраивал. И в исходно заявленных областях применения («написание UNIX»), и в других. Но теперь за развитие языка взялись люди, которые подрывают сложившиеся устои. Оставаясь в рамках формально допустимого, они тем не менее лишают язык той привлекательности, которую он имел. Сложность применения языка приближается и превосходит сложность ассемблера. Область применения ограничивается искусственно («только UNIX»). Зачем? Хочется убить хороший язык? Да нет проблем. Когда наберется критическая масса людей, которым надоели эти фокусы с наказаниями программистов за Undefined Behavior — сделают новый язык. Благо нынче это легче, чем 20 лет назад.
Когда наберется критическая масса людей, которым надоели эти фокусы с наказаниями программистов за Undefined Behavior — сделают новый язык
Я только за. Просто это — дело небыстрое.
Си тоже требует рантайма, и немалого. Посмотрите в стандарт. Программисту гарантируется наличие стандартной библиотеки, и он может пользоваться любыми функциями из нее. А это уже рантайм.

Стандарт C на самом деле допускает так называемые freestanding implementation, без рантайма.
Он требует какого-никакого рантайма

А Си не требует? malloc из воздуха берется?
Вполне можно взять object pascal диалект, обкромсать его до уровня stub'ов и будет он без рантайма. Например, программа hello world на Delphi укладывается в 1кб, с учетом заголовков PE, выравнивания и т.д.
malloc не возникает если его не вызвать. А в pascal те же строки с динамическими массивами требуют GC и хукнуть их нельзя.
Так в Си строк как таковых вообще нет. Так что 1:1.

GC там идет через установку скрытых try..finally.
Не используйте строки и динамические массивы — и будет вам счастье. Вызывайте менеджер памяти ОС и используйте ASCIIZ C-style. Более того, try-..finally затыкается stub'ми.
Расскажу историю из своего опыта, полных подробностей, к сожалению не вспомню, но суть такова: разрабатывали у нас в начале 2000х на работе железку, основной задачей которой являлась связь борта со спутником. Железка сделана на специфической встраиваемой платформе и крайне сурово. В итоге она была «замурована» в глубины фюзеляжа здоровенного А-50, под многие слои кабелей и всего остального и отправлена вместе с бортом для испытаний в Израиль. Техническое обслуживание этой железки, в т. ч. заливка новой прошивки и плановые диагностические процедуры, которые проводились по расписанию, но крайне редко, осуществлялись с помощью своего протокола, работающего поверх UDP. После завершения тестирования борт отправился в Индию, где плановые работы осуществлял уже техник заказчика, который приезжал на аэродром с ноутом и проводил там все необходимые операции. Так вот, во время одной из таких плановых проверок, внезапно обнаружилось, что железка на команды технической управляющей программы не отвечает от слова совсем и создаётся полное впечатление, что «паночка помэрла». После изучения проблемы и попыток повторить ошибку выяснилось, что в качестве значения необязательного поля контрольной суммы UDP заголовка железка всегда ожидает нуль и ничего другого. Всё это прекрасно работало и прошло всю суровую военную приёмку потому, что Windows до Vista никогда это поле не заполняла и там всегда был 0, а потому и криво реализованную проверку контрольной суммы, которую компилятор успешно оптимизировал в нерабочую реализацию, абсолютно никакие тесты не выявили.

Мораль сей истории в том, что стандартам надо следовать и написанное не по стандарту, прекрасно работающее сегодня и проходящее все тесты, завтра или в полнолуние или во время коррекции на добавочную секунду может превратиться в тыкву. В этой истории всё ещё обошлось подключением ноутбука с XP для перезаливки прошивки и очень хорошо закончилось лишь лишением премиальных за текущий месяц всех непосредственно причастных и дополнением описания процедур проверки и тестирования. Но ведь подобный баг в другом месте мог закончиться необходимостью срочного выковыривания железки из недр борта или того хуже, внезапным её отвалом во время военной операции. Подобное происшествие уже привело бы к обрыву связи и полной потере группировкой информации о боевой обстановке, а, возможно, и потере руководства операцией ибо А-50 может использоваться и как командный борт. Ситуацию ещё усложнило бы то, что поскольку железки одинаковые, то никакое горячее резервирование не помогло бы от одного и того же бага в софте.
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
Вот вам еще код ломающий мозг — MinGW и cl дают разные результаты

#include
int main()
{
unsigned char cbool = 0;
bool* pbool = (bool*)&cbool;
bool obool, ibool;
do
{
obool=!pbool[0];
ibool = pbool[0]==true;
printf("%s -> %i %i %i\n", pbool[0]?«TRUE»:«FALSE», (int)pbool[0], (int)obool, (int)ibool);
cbool++;
}
while(cbool);
}
тут разве не нарушение strict aliasing?
Это про «брутальное» кастование указателей? Да — но фишка не в том, логическое инвертирование в некоторых компиляторах будет инвертировать только крайний левый бит, а выражение хранящие в себе число хранящее значение отличное от 0 будет истинным при тернарнике, но ложным при сравнении TRUE.
UFO just landed and posted this here
Ведь никакого разыменования (то есть чтения памяти по указанному адресу)

Это не эквивалентные вещи: «разыменование» относится к семантике программы, а «чтение памяти» — к её низкоуровневой реализации.

Компилятор может транслировать одно в другое, а может и не транслировать — его личное дело.

Вот старый пример:
#include <stdio.h>
#include <stdlib.h>
 
int main() {
  int *p = (int*)malloc(sizeof(int));
  int *q = (int*)realloc(p, sizeof(int));
  *p = 1;
  *q = 2;
  if (p == q)
    printf("%d %d\n", *p, *q);
}


Как по-вашему, осуществляется разыменование в последней строчке, или нет?
UFO just landed and posted this here
«Осуществляется, но в программе его может и не быть»?
Т.е. «осуществляется, но может и не осуществляться»?
UFO just landed and posted this here
Вот в этой строчке написано, что нужно считать память по указанным адресам

Нет, стандарт языка ничего не говорит про память и про адреса.
Использование памяти и адресов — на ответственности конкретного компилятора.
Он имеет право вообще не использовать память ни для p, ни для q, ни для *p, ни для *q, если у него достаточно свободных регистров.
Я правильно понимаю, что согласно этому:
если lvalue-выражение не указывает на объект при своем вычислении, возникает неопределенное поведение.

выражение
#define offsetof(st, m) ((size_t)(&((st *)0xdeadbeaf)->m)-0xdeadbeaf)
так же приводит к UB?
Эквивалентный ассемблер
#define offsetof3(st, m) ((size_t)(&((st *)0xdeadbeaf)->m)-0xdeadbeaf)
  auto o3 = offsetof3(xx, x1);
011F17DA  mov         dword ptr [o3],0  
  auto o4 = offsetof3(xx, x2);
011F17E1  mov         dword ptr [o4],4  



Т.к. в примере выше, очевидно, вычисление происходит в compiletime, тогда это:
intptr_t fake_ptr=0xbadf00d;
#define offsetof2(st, m) ((size_t)(&((st *)fake_ptr)->m)-fake_ptr)
c точки зрения того же правила, не может быть UB. Но логически ничего не поменялось — разыменование заведомо неверного адреса.
Эквивалентный ассемблер
  intptr_t fake_ptr=0xbadf00d;
012C17BE  mov         dword ptr [fake_ptr],0BADF00Dh  
#define offsetof2(st, m) ((size_t)(&((st *)fake_ptr)->m)-fake_ptr)
  auto o1 = offsetof2(xx, x1);
012C17C5  mov         eax,dword ptr [fake_ptr]  
012C17C8  sub         eax,dword ptr [fake_ptr]  
012C17CB  mov         dword ptr [o1],eax  
  auto o2 = offsetof2(xx, x2);
012C17CE  mov         eax,dword ptr [fake_ptr]  
012C17D1  add         eax,4  
012C17D4  sub         eax,dword ptr [fake_ptr]  
012C17D7  mov         dword ptr [o2],eax  

Про nullptr из стандарта: Если константа нулевого указателя приводится к типу указателей, то результирующий указатель, называемый нулевым, гарантированно будет не равен указателю на любой объект или функцию.
А вот 0xbadf00d вполне может указывать на какой то объект или функцию. Так что UB рассматриваемого в статье типа не будет. Но возможно есть другое. :) Не знаю.
Вообще никто не мешает использовать в качестве nullptr 0xdeadbeef, лишь бы система в целом была в курсе.
По стандарту NULL должен кастоваться в ноль.
разговор не про C и NULL, про c++ и nullptr, вернее ghj с++.11 и далее.
Вряд ли, иначе выражения if(ptr) будут всегда истинными.
В ваших примерах, несмотря на то, что оператор разыменования в коде присутствует, фактического чтения данных по этому адресу не происходит, всё остается на уровне арифметики адресов. На уровне стандарта терминология, позволяющая разграничить эти два случая, тоже существует: оператор разыменования генерирует lvalue (адрес), а процесс чтения данных по адресу осуществляется с помощью lvalue-to-rvalue conversion. Но, видимо, авторы стандарта не захотели усложнять/поленились/не учли этого, в общем, по какой-то причине не разграничили эти случаи.
Отличие от nullptr тут одно. С nullptr — это гарантированное UB на уровне языка в данной конкретной строке кода, независимо от поведения других частей программы. А с fake_ptr UB, если разработчик заведомо знает, что под fake_ptr реально не размещен объект (то есть, нужно знать как работает вся программа и даже оборудование, вдруг оно размещает по данному адресу объект).
Я посмотрел на заголовок статьи — и захотел вмешаться. Потом прочитал текст и понял что несогласие моё сугубо терминологическое, по конкретным примерам что вы привели — претензий нет.

Раз уж вы упомянули 232. Is indirection through a null pointer undefined behavior? — то там вроде ж пришли к выводу, что разыменовать нулевой указатель можно, и к неопределенному поведению это не приводит. Получается лишь lvalue, которое никуда не указывает.

А вот дальше с этим lvalue особо ничего сделать полезного нельзя. Адрес взять — вроде можно, сконструировать на него ссылку — нельзя, применить оператор "->" — если верить вам — нельзя. Но зато можно позвать typeid() и он бросит исключение bad_typeid.
Ваша логика опирается на понятие «указатель указывающий на объект». Но это понятие четко нигде в своей логике вы не описали. Почему у вас получается что (*(0))->fld не указывает на объект а (*(x))->fld, где x != 0 уже указывает?
Мне непонятно, почему нулевой адрес считается заведомо некорректным. Нет, точнее понятно — в подавляющем большинстве случаев, реализаций, так и есть. Но это может быть и не так — вполне может оказаться, что в каком-то экзотическом софте адрес 0 доступен.
Потому, что Си изначально создан как легкопортируемый язык, который в гробу видал все особенности реализации.
Вы доказываете мне же мою же точку зрения.

О том и речь — если язык портируемый, то какого… зачем, в общем, опираться на особенности реализации какой-то ОС? Если в реализации xyz указатель 0 — некорректный, то это проблемы данной платформы, а не языка. Который портируемый. На разные платформы, ага.
Потому, что Си неявно определяет свою собственную виртуальную машину. Да, эта машина очень низкоуровневая, имеет прямой доступ ко всем возможностям хоста, на практике всегда транслируется в машинные коды хоста, но это все равно виртуальная машина. Эта машина была придумана когда о x86 и AVR еще и не думали. И в ней 0 — некорректный указатель.
Вас не затруднит указать первый стандарт, в котором появилось упоминание этого?
До x86 и AVR были другие архитектуры, зачастую с перекрестными окнами и отдельными внешними аппаратными менеджерами памяти. В которых 0 тоже мог быть корректным адресом.
Мог быть, а мог и не быть. И любая программа должна была работать независимо от этого. Переносимость Си не означает, что он позволяет любую платформу использовать на 100%. Наоборот, она означает, что любая корректная программа переносится на любую платформу, независимо от ее особенностей.
По стандарту нулевой адрес может быть вполне корректным. Язык C (а также C++) вводят понятие null указателя. Это указатель с определенным зарезервированным значением, которое сигнализирует, что указатель не указывает никуда. Это значение не обязано представляться физическим нулем, может быть, что угодно, например, 0xffffffff. Но, т.к. это представление сильно зависит от платформы, устанавливать значение указателя в особое состояние null необходимо присваиванием нулевого значения целого интегрального типа. То есть int* p = 0; инициализирует разрядную сетку указателя значением 0xffffffff, и при этом p == 0 — истина, if (p) будет ложно.
И это не только в теории. Есть системы, где null указатель не представлен физическим нулем: c-faq.com/null/machexamp.html
Ага, значит, мы можем переопределить null на конец памяти? Это, кстати, интересная идея и даже более здравая — никто не сможет хранить структуру (объект) в конце памяти. А в начале может.

Тут путаница с самим словом NULL. В том же Паскале используется nil, оно чуть-чуть более нейтрально.

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

А почему не значения null, которое могло бы быть переопределено? Определено оно для платформы как 0 — будет 0, -1 — будет 0xff....ff?
Из-за проверки if, которая опирается на нулевок значение, а не на null в случае указателя?
Ага, значит, мы можем переопределить null на конец памяти?

Если Вы о макросе NULL, то его переопределение вряд ли чем-то поможет, а скорее навредит. Подозреваю, что везде он будет определен как ноль, с различными вариациями типов 0, ((void*)0), ((int)0).
Само же особое состояние null указателя переопределить никто не может, оно задается компилятором. Мы лишь можем присвоить это состояние через p = 0 или p = NULL и сравнить с тем же 0 или NULL.
Тут путаница с самим словом NULL. В том же Паскале используется nil, оно чуть-чуть более нейтрально.

Как раз NULL и был призван решить эту путаницу, чтобы «0» не маячил перед глазами и не давал ложного ощущения, что p=0 — это присваивание адреса нулевой ячейки, p = NULL уже яснее. NULL не значит zero, NULL значит пустой, как в SQL.
Кстати, я смотрю дискуссия разделилась на независимые ветки, и нашу ветку не читают :), товарищи в других ветках продолжают наезжать на стандарт, за то, что он якобы не позволяет размещать данные в нулевой ячейке, вот наглядный пример интерпретации, якобы p = 0 обязательно настраивает указатель на нулевую ячейку памяти.
А почему не значения null, которое могло бы быть переопределено?

Этого я не знаю. Предположу, что по тем же причинам, что и отсутствие типа bool в C. Для нового значения null, нужно было вводить новый тип данных. Наверное, встроенные типы языка C должны были соответствовать типам платформы. В C++ же добавили nullptr с типом nullptr_t. Опять же, переопределить его никто не даст, также никто не обязан знать его физическое представление.

Подытожу:
1. #define NULL лишь для удобства чтения, на нем нет смысла зацикливаться, его переопределение ничего хорошего не даст, уверен, что на всех платформах он определен как 0 (с типом void* или без него или еще с каким-либо типом, но все равно 0).
2. Каждая платформа имеет свой зарезервированный пустой указатель. О его реальном физическом представлении знает лишь компилятор.
Мы лишь можем присвоить это состояние через p = 0 или p = NULL и сравнить с тем же 0 или NULL.

Но раз вводится отдельная сущность — NULL, в этом есть смысл, кроме повышения читаемости кода. Раз уж это макрос. Логично было бы, если бы if для указателей сравнивал бы с !NULL, а не !0.

2. Каждая платформа имеет свой зарезервированный пустой указатель. О его реальном физическом представлении знает лишь компилятор.


Верно, но тогда компилятор какой-либо платформы может/должен учитывать, что NULL, как некорректный адрес, не равен (может быть) 0, потому что 0 — корректный.

Т.е. если говорить о переносимости, то на двух платформах:

#define NULL ((void*)0)

p = NULL;


if(p){

}else{

printf(«we must be here\r\n»);
}

и

#define NULL ((void*)0xFFFFFFFF)

p = NULL;


if(p){

}else{

printf(«we must be here\r\n»);
}

Должен в обоих случаях выдать надпись — ведь правила переносимости соблюдены — при вроверке указателя идет сравнение с NULL.

Предположу, что по тем же причинам, что и отсутствие типа bool в C. Для нового значения null, нужно было вводить новый тип данных.

Так тип данных-то есть — указатель это не int. И на множестве экзотических платформ разрядность указателя может не совпадать с int'ом, поэтому pointer есть как тип. А вот введение отдельного bool это уже лишняя сущность, плюс почти везде сравнение с нулем «дешевое». Указатели — чуть другое дело.
UFO just landed and posted this here
Я не это имел ввиду. Автор в своем доказательстве опирается на описаный мной момент, который не имеет спецификации, и тем самым пытается описать неопределенное поведение.
Мне кажется ответ в статье есть. Быть может не очень удачен перевод фрагментов из стандарта, но можно поискать в оригинале.
Если константа нулевого указателя приводится к типу указателей, то результирующий указатель, называемый нулевым, гарантированно будет не равен указателю на любой объект или функцию.
Возможно, я не прав, тогда пусть меня поправят.
Я предлагаю вот таким способом разрешить спор. Если я правильно понял стандарт, я могу написать свой компилятор, который, следуя букве стандарта, имеет полное право на каждый оператор p->x (в том числе &p->x) «трогать память» под переменной x (считывать в регистр, в отладочных целях, например). Вопрос эффективности не волнует, зачем это делать — тоже не важно. Ведь задача стандарта — быть максимально абстрагированным и не делать предположений о реализации.
В таком случае мой компилятор будет соответствовать стандарту, а ((T*)nullptr)->x будет стабильно падать. Также на моем компиляторе будет стабильно падать пример из комментария выше. Следовательно, оба примера являются UB.
Осталось доказать, что мой компилятор соответствует стандарту, и я ничего не упустил.
Насколько я слышал, разработчики ядра Linux видят компилятор языка C скорее как макроассемблер, который всегда выдает предсказуемый результат. Поэтому для них не очень-то важны эти тонкости стандарта, ведь они знают, что этот код скомпилируется в простое арифметическое сложение адреса и смещения. И именно поэтому для сборки ядра всегда используется один и тот же компилятор (GCC).
Значит, ядро написано уже не на C99, а на GCC C, и это не разные языки. Вообще, так быть не должно.
Вообще, так быть не должно.
Скажите это Торвальдсу :)
UFO just landed and posted this here
Логику разбора определяет стандарт. А стандарт говорит, что компилятор не обязан корректно разбирать этот код. Наткнувшись на такое, компилятор имеет полное право поджечь компьютер или захватить Землю. И будет прав.
UFO just landed and posted this here
Компилятор не знает какое значение может быть у переменной в момент обращения
Что компилятор знает, а что нет — это не ваше дело. Он вообще может ничего не компилировать, а слетать в будущее и скачать оттуда уже откомпилированную программу. Или прочитать ваши мысли. Стандарт это не запрещает. И в современных компиляторах оптимизатор очень часто знает, что переменная равна/не равна нулю.
Банальный разбор выражения: &p->val будет идти по логике

Нет. Там где есть UB, нет никакой логики. Не придумывайте сценарий. Как только в коде встречается UB, дальше может быть всё что угодно.
UFO just landed and posted this here
Вы не понимаете, что такое неопределённое поведение. Компилятор вообще вправе не генерировать код, если в программе есть UB. Вы пытайтесь доказать, что земля плоская. В опровержение, Вы требуете показать конкретно пальцем место где она закругляется. :)
Вопрос не в том, как идет разбор при UB, а в том, зачем корректную ситуацию представлять как UB.
Здесь нет корректной ситуации. Здесь разыменование нулевого указателя. То что код корректен, это фантазии.
UFO just landed and posted this here
если же будет define NULL 0x4234434

Такого не будет.

А если будет, то обсуждаемая проблема ничто, по сравнению с теми, которые появятся. :)
UFO just landed and posted this here
UFO just landed and posted this here
Стандарт C устроен таким образом, что компилятор не обязан корректно обрабатывать вещи, которые на разных платформах работают по разному. Даже если на конкретной целевой платформе какой-то UB имеет вполне логичную интерпретацию, компилятор не обязан ее придерживаться: стандарт этого не требует.
UFO just landed and posted this here
Разговоры о переносимости вообще страннЫ в данном контексте. Да, код на Си скомпилируется и на другой платформе — в этом смысле он переносим.
Но если это у нас написанный на Си драйвер под Windows, переноси — не переноси — программа все равно платформозависимая.
NULL далеко не «всего лишь define» с точки зрения оптимизатора.
UFO just landed and posted this here
А вас интересует конечный продукт компиляции, который проходит через оптимизатор. А оптимизатор имеет право делать с некорректным кодом что угодно.
Эта оптимизация, значит, платформозависимая.
Я неверно понял, что подразумевается под «нулевой указатель». Мне казалось, что «нулевой» — это всегда численно равный нулю. И в этом случае — разыменовывай — не хочу.
Но, как верно отметили выше, нулевой — это NULL, который может теоритически и не быть численно ранвым 0.
Что-то я крайне сомневаюсь, что тут есть какое-то UB.
Читаем стандарт, нам понадобятся два пункта:

6.2.5 Types
A structure type describes a sequentially allocated nonempty set of member objects
6.5.3.2 Address and indirection operators
The unary & operator yields the address of its operand. If the operand has type ‘‘type’’,
the result has type ‘‘pointer to type’’. If the operand is the result of a unary * operator,
neither that operator nor the & operator is evaluated and the result is as if both were
omitted.

Ну а дальше начинаем толковать стандарт:

&podh->line6
/*1*/ = &(*podh).line6
/*2*/ = &(*(struct usb_line6*)((char*)podh + offsetof(struct usb_line6_podhd, line6)))
/*3*/ = (struct usb_line6*)((char*)podh + offsetof(struct usb_line6_podhd, line6))

Первый переход просто «по определению», второй — потому что структуры состоят из «последовательно расположенных непустых полей», ну а (3) даже специально прокомментирован в стандарте (страница 89, сноска 102 внизу страницы):

Thus, &*E is equivalent to E (even if E is a null pointer)

Где здесь UB?
Вы написали совсем другой код. Не надо придумывать, как может работать компилятор. Он может поступить так, а может по-другому. Как хочет.
Я не придумываю. Я привел две цитаты из стандарта, которые трактуются весьма однозначно и не оставляют поля для маневра.
В плюсах для не POD-типов есть нюасны, но стандарт плюсов — это совсем другая история.
Приведенная вами цитата

в Разделе 6.3.2.3 «Указатели» сказано следующее:
Если константа нулевого указателя приводится к типу указателей, то результирующий указатель, называемый нулевым, гарантированно будет не равен указателю на любой объект или функцию.

рекомендуется к повторному прочтению на языке оригинала:

If a null pointer constant is converted to a
pointer type, the resulting pointer, called a null pointer, is guaranteed to compare unequal
to a pointer to any object or function.

Это означает, что any_valid_object_or_function_pointer != NULL всегда истинно.
Трактовка «нулевой указатель не указывает на объект, поэтому к его полю нельзя обратиться даже для вычисления адреса» смахивает на подмену понятий.

Вообще говоря, давайте вместе почитаем вот это (6.3.2.1 Lvalues, arrays, and function designators, п 2):

Except when it is the operand of the sizeof operator, the _Alignof operator, the
unary & operator, the ++ operator, the — operator, or the left operand of the. operator
or an assignment operator, an lvalue that does not have array type is converted to the
value stored in the designated object (and is no longer an lvalue); this is called lvalue
conversion.

Я долго думал, а потом прочитал это как

Кроме случаев, когда lvalue является аргументом операторов sizeof/alignof/унарного &/инкремента/декремента или левой частью оператора «.» или присваивания, lvalue, не являющееся массивом, преобразуется к значению, хранящемуся в означенном объекте.

Это по факту говорит нам, что чтения значения из lvalue при взятии от него адреса не происходит (а также sizeof(podh->line6), _Alignof(podh->line6)) => нет никакого разыменования => нет UB.
Учтите, что фактически Вы оспариваете не моё мнение, а нескольких человек. Например, члена комитета стандартизации языка Си++ Габриэля Дус Рейса. Я только постарался собрать их ответы в единый связанный текст.

Для меня, например, вопрос закрыт. Их мнение для меня более веско.

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

Мне в конечном итоге ведь всё равно, есть здесь UB или нет. Я хотел выяснить этот вопрос и поделиться результатом исследования, так как этот момент многих интересует. Поэтому, если Вы окажетесь правы, то это будет хорошее дело. Пока же мнение 4 солидных (и одного меня, пусть не солидного :) перевешивают мнение таинственного dimoclus без единой публикации.
Я ни в коем случае не лезу в язык C++ — у него свой стандарт, гораздо более сложный и объемный. И я совершенно согласен, что в C++ для произвольного объекта выполнять «->» опасно, т. к. перегруженный оператор может вызывать виртуальные функции/обращаться к полям объекта, что приведет к «invalid behaviour».
Однако я цитирую стандарт C и linux kernel, фрагмент кода которого приводится, написан на языке C, по стандарту которого UB нет.
Хочу еще раз заметить, что все ваши рассуждения сводятся к «объект не может находится по адресу NULL», чего в стандарте явно не написано (см ваш перевод и оригинальный текст, которые я привел).

>перевешивают мнение таинственного dimoclus без единой публикации
Будьте любезны не скатываться на личности и уж тем более не приводить число публикаций на хабре в качестве объективного критерия компетентности.
Спасибо за корректные цитаты.
Я перевёл Ваши комментарии и попросил Габриэля Дус Рейса прокомментировать их. Признаю, из-за перевода туда-сюда, качество текста снижается, однако суть сохранилась. Итак, его ответ:
Дебаты насчет UB и вправду бывают подчас утомительными – заявляю, как председатель исследовательской группы по изучению «неопределенного и неуточнённого поведения» при комитете по C++ :). Подозреваю, что Вы едва ли сможете переубедить тех, кто уже поверил в отсутствие в обсуждаемом коде неопределенного поведения.

В цитируемой Вами дискуссии наблюдается путаница насчет того, что будто бы есть какая-то разница между POD и не-POD. На самом деле, такой разницы нет.

Также нужно заметить, что Пункт 6 в C++-стандартах касается statement. Выражения же обсуждаются в Пункте 5, так что не совсем понятно, какой же стандарт цитируют люди – C или C++. Конечный результат, однако, остается тем же – неправильное описание offsetof провоцирует неопределенное поведение.

Наконец, неопределенное поведение происходит из попытки обратиться к полю ПОСЛЕ разыменовывания нулевого указателя – само по себе разыменовывание нулевого указателя не ведет к UB в C++11 и C++14.

Надеюсь, это поможет.

еопределенное поведение происходит из попытки обратиться к полю ПОСЛЕ разыменовывания нулевого указателя – само по себе разыменовывание нулевого указателя не ведет к UB


Ага, т.е. взятие адреса в обсуждаемом примере таки не UB.
Ведь при &p->field обращения к полю нет.
Подозреваю, что Вы едва ли сможете переубедить тех, кто уже поверил в отсутствие в обсуждаемом коде неопределенного поведения.
Вот я и пытаюсь понять: каким образом в ответе стыкуется «есть UB» и «только при попытке обращения к полю», если обращения к полю нет.
Это только с точки зрения компилятора/оптимизатора взятие адреса в данном случае не является разыменованием, соответственно и UB. Однако с точки зрения стандарта текст программы нужно читать буквально: есть "->" — есть разыменование, а если по указателю может быть 0 — то и неопределенное поведение. Собственно конкретная работа оптимизатора никак не описана в стандарте и, соответственно, является UB. А для соответствия стандарту от компилятора требуется строго ожидаемого поведения от буквально прочитанного кода, поэтому неопределенность оптимизатора никогда не проявляется в корректном коде. А если в этом коде присутствует формальный UB (даже если с точки зрения компилятора это не так), можно ожидать проявления UB со стороны оптимизатора.
буквально: есть "->" — есть разыменование, а если по указателю может быть 0 — то и неопределенное поведение.

В ответе сказно:

неопределенное поведение происходит из попытки обратиться к полю ПОСЛЕ разыменовывания нулевого указателя – само по себе разыменовывание нулевого указателя не ведет к UB


В куске &p->a нет обращения к полю а.
Взятие адреса от несуществующего объекта &p->a ведёт к UB, ибо взятие адреса должно оперировать с существующим объектом.

Операнд унарного оператора & должен быть либо указателем функции, либо результатом оператора [] или унарного оператора *, либо lvalue-выражением, указывающим на объект, который не является битовым полем и не содержит в объявлении спецификатора регистрового класса памяти.

Выражение 'podhd->line6' однозначно не является указателем функции, результатом оператора [] или *. Это как раз lvalue-выражение. Однако, когда указатель 'podhd' равен нулю, выражение не указывает на объект, поскольку в Разделе 6.3.2.3 «Указатели» сказано следующее:

Если «lvalue-выражение не указывает на объект при своем вычислении, возникает неопределенное поведение» (Стандарт C99, Раздел 6.3.2.1 «Lvalue-выражения, массивы и указатели функций»):
Например, такой код:
 struct usb_line6_podhd *podhd;

Обычное объявление указателя, значение указателя не определено. Возникает-ли здесь UB и надо-ли бить тревогу? Думаю, что нет. С точки зрения поведения компилятора/стандартов ситуация решается однозначно. С точки зрения поведения программы — зависит о того, как этот указатель будет использоваться дальше (чтобы вынести вердикт, нужно анализировать остальной код).

Возьмем другой пример:
int foo(int a)
{
   int b = a + 100500;


Обычное сложение. Возникает-ли здесь UB, и нужно-ли бить тревогу? С точки зрения стандарта/компилятора все однозначно — какое-то значение у входного параметра будет. С точки зрения программы — зависит от значения на входе. Если прилетит значение в районе INT_MAX, будет переполнение. Является-ли такой код проблемным? Чтобы вынести вердикт нужно анализировать остальной код. Возможно тут есть место для логической ошибки, и стоит добавить проверку на граничные значения входного параметра. В общем, это нормальная программисткая ситуация, когда нужно определяться с логикой проверки входных данных, а не UB.

Теперь смотрим пример из топика:
static int podhd_try_init(struct usb_interface *interface,
        struct usb_line6_podhd *podhd)
{
struct usb_line6 *line6 = &podhd->line6;

Если-по простому, здесь записано выражение «к podhd прибавить смещение line6 в структуре usb_interface». Т.е. не смотря на количество закорючек, тут записана такая же простая арифметика, как во втором примере. Только это арифметика с указателями. Ни какого разыменовыания здесь нет (тут уже неоднократно про это писали, в т.ч. вы сами цитатой эксперта). Можно-ли говорить тогда про UB? С точки зрения стандартов/компилятора выражение интерпретируется однозначно, поэтому UB нет. С точки зрения логики программы — зависит от значения указателя, которое прилетит на вход функции. Может прилететь правда адрес структуры, может NULL, а может какой-то бред (если передадут неинициализированный указатель, как в первом примере). Но в «С» не запрещается хранить в указателях «непонятно что» (см. первый пример). Тут может иметь место логическая ошибка (случай когда нужна проверка входных параметров функции), но само по себе это не UB.
Нет ничего неправильного в том, чтобы писать небезопасный код — не нужно платить за то, что не используется, если аргумент, приводящий к UB, по логике программы не может быть использован, обработкой исключений можно безболезненно пожертвовать.

struct usb_line6 *line6 = &podhd->line6;

Таким образом здесь мы соглашаемся, что podhd!=nullptr, и подобный аргумент не должен быть использован при вызове функции. Неопределенного поведения в этой строке нет.

if ((interface == NULL) || (podhd == NULL))

Однако дальнейшая проверка podhd уже является неопределенным поведением, так как существуют два конкурирующих способа осуществить эту проверку, возможно дающих разный ответ: проверить значение указателя, или воспользоваться ранее выведенной аксиомой, что podhd!=nullptr.
Таким образом здесь мы соглашаемся, что podhd!=nullptr

Возражаю! :) В общем случае такая эвристика не уместна, у Andrey2008 про это было:
А есть ли какая-та ситуация, когда при P == nullptr мы напишем &P->m_foo и всё будет хорошо? Да, например это может быть аргументом оператора sizeof: sizeof(&P->m_foo).

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

В стандарте нигде не определено, считать ли такой случай разыменованием или нет. Аргументом к тому, что «считать», может быть пример ниже про виртуальное наследование. А так как в стандарте ничего не сказано, про то, что данный случай «не считается разыменованием», это и называется «неопределенным поведением», так как стандартом оно не определено.
Тут с первой строчки было понятно, что топик обречен на успех.
Ненароком я породил большую дискуссию, касающуюся того, допустимо ли использовать в Си/Си++ выражение &P->m_foo,

Это неизбежно, когда ненароком ставят рядом два довольно-таки разных языка (более эпичный вариант лишь Java/JavaScript). ;)

Но эту ветку комментов конкретизировал dimoclus:
Я ни в коем случае не лезу в язык C++ — у него свой стандарт, гораздо более сложный и объемный.

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

Для С вопрос разъясняется в:
102) Thus, &*E is equivalent to E (even if E is a null pointer), and &(E1[E2]) to ((E1)+(E2)). It is always true that if E is a function designator or an lvalue that is a valid operand of the unary & operator, *&E is a function designator or an lvalue equal to E. If *P is an lvalue and T is the name of an object pointer type, *(T)P is an lvalue that has a type compatible with that to which T points.

Отсюда напрямую следует вывод, что если компилятор С вздумает разыменовывать указатели при интерпретации выражения, то свойство эквивалентности будет нарушено (как раз для случая null pointer, который тут особо отмечен, ибо в процессе он словит UB). Поэтому так делать нельзя. Поэтому нормальный компилятор С так делать не будет и с точки зрения программиста на С тут нет UB.

Мне кажется, что рационализм в проблеме, поднятой Andrey2008 есть. Делать прямые указатели на чужие внутренности может быть чревато по многим причинам. Поэтом интерес к вопросу, как разработчика статического анализатора, понятен. Просто вопрос этот скорее из области best practices программирования, а не спецификаций С.
Для С вопрос разъясняется в

Вы вырываете из контекста:
If an invalid value has been assigned to the pointer, the behavior of the unary * operator is undefined.102)

Among the invalid values for dereferencing a pointer by the unary * operator are a null pointer, an
address inappropriately aligned for the type of object pointed to, and the address of an object after the
end of its lifetime

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

Это неизбежно, когда ненароком ставят рядом два довольно-таки разных языка (более эпичный вариант лишь Java/JavaScript). ;)

Я мог бы с вами согласиться на 100%, если бы все четыре основных компилятора не были бы C/C++.
Вот пример на понимание:

int* b = (int *)NULL;
int* c = &*b;
*c;


Определено-ли «c» при объявлении во второй строчке? Да ( &*E is equivalent to E (even if E is a null pointer)).
Будет-ли в третьей строке UB при попытке разыменовать *c? Да (по правилу на которое вы ссылаетесь).

В примере «struct usb_line6 *line6 = &podhd->line6;» вычисляется указатель на line6, т.е. смещение line6 относительно podhd. Смещения не зависят от значений, только от типов. Тут нет разыменовавания, и пункт на который вы ссылаетесь не подходит. Поэтому я о нем не говорил (а не вырвал из контекста, как вы выразились).

Я мог бы с вами согласиться на 100%, если бы все четыре основных компилятора не были бы C/C++.

Вы на 100% не сможете скомпилировать программу на «C/С++»: это будет программа либо на «С», либо на «С++».
Выше я приводил цитаты из стандарта языка C, где черным по белому написано, что в конструкциях &ptr->field и &*ptr никаких разыменований нет. И даже есть сноска, специально поясняющая, что &*(int*)NULL — это ок с точки зрения языка.

В C++ есть нюансы, называемые «перегрузка операторов», где может встретиться вызов виртуальной функции, что требует валидного объекта для чтения vmt. Поэтому в плюсах &objptr->field действительно в общем случае UB. В C же отсутствие UB в таких случаях гарантировано стандартом.

Большая ошибка полагать, что C — это как C++ (и делать на основании этого выводы), только без классов, оверлоадинга и перегрузки операторов. Это разные языки, но с похожим синтаксисом.
Ещё неприятность в том, что операторы & и -> могут быть перегружены.
Статья не ограничивается одним только C.
UFO just landed and posted this here
Нет, статья не ограничивается исходным кодом. Исходный код дан в статье, как пример. Так же считаю, что код написанный на С должен без проблем компилироваться на C++, т.к. довольно частое явление копипаста при переписывании функционала из С в С++. Яркий тому пример GCC переписанный с С на С++. Не удивлюсь, если ядро линукса тоже будет переписываться на C++.
Так же согласен с автором статьи, что даже в С получение указателя на поле невалидного объекта может вести себя непредсказуемо, и зависит от платформы и компилятора. А т. к. поведение не определено стандартом, то это является использованием недокументированных возможностей, которого следует избегать.
UFO just landed and posted this here
А будет ли неопределённое поведение в таком гипотетическом случае?
static int podhd_try_init(struct usb_interface *interface,
        struct usb_line6_podhd * volatile podhd)
{
  int err;
  struct usb_line6 *line6 = &podhd->line6;

  if ((interface == NULL) || (podhd == NULL))
    return -ENODEV;
  ....
}
Думаю разницы нет. Компилятор может обращаться с переменными так-же, как с volatile переменными. Соответственно, volatile ничего нового в код не добавляет.
Скажем так. С наибольшей вероятностью этот код будет корректно работать при сборке в релизе из-за отложенной инициализации переменных. Т.е. если перефразировать, то код
struct usb_line6 *line6 = &podhd->line6;

окажется явно ниже проверки:
if ((interface == NULL) || (podhd == NULL))

Исключает ли этот код UB? — нет, не исключает, оно никуда не делось.
Можно ли так писать? — нет, нельзя, потому что в коде UB.

P.S. почитайте ради интереса историю, что я выше привёл. Пусть и не помню я точных подробностей, но проблема была в коде, примерно, из-за аналогичных причин.
Проблема в неверной постановке вопроса:
«разыменовывание нулевого указателя» однозначно по стандарту является неопределенным поведением.
А вопрос на самом деле заключается в следующем: считается ли конструкция "&a->b" разыменованием указателя a.
Поясню:
Происходит ли здесь присваивание?
x = x;


Суммирование?
y = x + 0;


Разыменование?
&a->b
Является.

Рассмотрим виртуальное наследование.
struct A { int x; };
struct B : virtual A { int y; };
struct C : virtual A { int z; };
struct D : B, C { int t; }

D* pd = rand() ? new D() : nullptr;

C* pc = pd; //  !pd ? nullptr : &(B&)*pd // нуль переходит в нуль, для объекта вычисляется адрес смещённой базы

A* pa = pc; //  !pc ? nullptr : &(A&)*pc // нуль переходит в нуль, для объекта берётся смещение базы и вычисляется её адрес
A& ra = *pc; // берётся смещение базы из скрытого поля объекта... (ой!)
int* px = &pc->x; // &((A&)*pc).x; // и снова ой!

Мы не знаем наверняка, что pc указывает только на D или nullptr, поэтому статически вычислять смещения не можем.
Самое интересное, — почему такое UB вообще может выстрелить.
Дело в том, что некоторые компиляторы (gcc 4.8+) агрессивно оптимизируют. И если они видят код
Some* x = get_from_outside();
.....
*x; // аха! разыменовываем, - значит,наверно, это можно! а почему можно? потому что не нуль.
if(x) // аха! там выше - не нуль, значит, здесь - if(true)
{
  do_really_dangerous_things(x); // спокойствие, только спокойствие, здесь не нуль.
}
else
{
  do_really_useful_things(); // нафиг надо, мы сюда в принципе не попадём
}

То мы выстрелим себе в ногу, во-первых, и не выполним какой-нибудь важный высокоуровневый контракт, во-вторых.
Вы правильно пишите, только это другой пример. =)

В топике не *x, а &*x, и как написали выше нет разыменовывания, а лишь простая арифметика с указателями:
Thus, &*E is equivalent to E (even if E is a null pointer)

А тем временем в кернеле исправляют данную ошибку, из-за которой весь цикл этих статей, после предупреждения coverity.

git.kernel.org/cgit/linux/kernel/git/next/linux-next.git/commit/?id=ef71a406ffabfa9645a5cdd6b37e11185e0a4f72
Тем, кому мало, вот здесь в комментариях продолжилось обсуждение нулевых указателей :). Добро пожаловать.
Sign up to leave a comment.