Вызывает ли макрос offsetof из ‹stddef.h› неопределенное поведение?

Пример из реализации MSVC:

#define offsetof(s,m) \
    (size_t)&reinterpret_cast<const volatile char&>((((s *)0)->m))
//                                                   ^^^^^^^^^^^

Как видно, он разыменовывает нулевой указатель, что обычно приводит к неопределенному поведению. Это исключение из правил или что происходит?


person Xeo    schedule 21.06.2011    source источник
comment
Обратите внимание, что трудно говорить о соответствии стандарту в файлах заголовков, поставляемых с реализацией. например Microsoft, обладающая контролем и знаниями о внутреннем устройстве своего компилятора, может делать там все, что захочет, при условии, что их компилятор/заголовочные файлы/библиотеки соответствуют стандарту в отношении пользовательского кода.   -  person Lyke    schedule 22.06.2011
comment
@Lyke: Что ж, работа, как и ожидалось, - это один из многих вариантов неопределенного поведения. Часто он же и самый опасный. :П   -  person Xeo    schedule 22.06.2011
comment
@Lyke Ваш комментарий должен быть ответом: компилятор предоставляет offsetofmacro, который работает должным образом. Компилятор также может реализовать memmove() со сравнением, которое привело бы к неопределенному поведению, если бы оно было в пользовательском коде. Все делают.   -  person Pascal Cuoq    schedule 22.06.2011
comment
Было бы неопределенно написать и использовать такой макрос в собственном коде; поэтому ваша реализация должна предоставить вам макрос offsetof, который вы можете использовать вместо него.   -  person Toby Speight    schedule 26.07.2018


Ответы (6)


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

(1) Соответствует ли код UB стандарту C++?

Это действительно сложный вопрос, потому что общеизвестным практически дефектом является то, что стандарт C++98/03 никогда прямо не говорит в нормативном тексте, что в общем случае разыменование нулевого указателя является UB. Это подразумевается из-за исключения для typeid, где это не UB.

Что вы можете сказать решительно, так это то, что использовать offsetof с типом, отличным от POD, — это UB.

(2) Является ли код UB по отношению к компилятору, для которого он написан?

Нет, конечно нет.

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

Ура и чт.,

person Cheers and hth. - Alf    schedule 22.06.2011
comment
Можете ли вы проверить, существует ли примечание, которое я нашел в [dcl.ref] (см. мой ответ), в C++03? - person Ben Voigt; 22.06.2011
comment
@Ben: он все еще существует в N3290 (т.е. C++0x, §8.3.2/5). Но примечания и примеры не являются нормативными в стандартах ISO. Например, примеры C++98/C++03 $5/4, в которых говорится о неопределенном поведении, неверны и противоречат непосредственно предшествующему нормативному тексту. - person Cheers and hth. - Alf; 22.06.2011
comment
Я знаю, что он существует в N3290, вот где я его нашел. Но у меня нет копии официального C++98 или C++03. - person Ben Voigt; 22.06.2011
comment
@Ben: это есть в исходном стандарте С++ 98, но на один абзац раньше, §8.3.2/4. Но, как я уже сказал, это не является нормативным. Это отсутствие нормативного текстового описания тем более сбивает с толку, что стандарт претендует на наличие такого описания: §1.9/4 Некоторые другие операции описываются в настоящем стандарте как неопределенные (например, эффект разыменования нулевого указателя). - но нет ссылки на то, где находится это предполагаемое описание... :-) Ура, - person Cheers and hth. - Alf; 22.06.2011
comment
никогда прямо в нормативном тексте не говорится, что в общем случае разыменование нулевого указателя является UB в любом случае, из того факта, что поведение разыменования нулевого указателя никогда не определено, следует, что это UB. - person curiousguy; 30.09.2011
comment
@AlfP.Steinbach Это просто стандартная ирония. - person curiousguy; 30.09.2011

Понятие «неопределенное поведение» неприменимо к реализации стандартной библиотеки, независимо от того, макрос это, функция или что-то еще.

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

Другими словами, макрос, который вы цитируете, не приводит к неопределенному поведению, если это конкретно макрос offsetof, определенный в стандартной библиотеке. Но если вы сделаете точно то же самое в своем коде (например, точно так же определите свой собственный макрос), это действительно приведет к неопределенному поведению. «Quod licet Jovi, non licet bovi».

person AnT    schedule 22.06.2011
comment
Точно. Общепринятой практикой является использование различных расширений компилятора, встроенных функций, встроенного ассемблера и т. д. (не разрешено в соответствующем приложении) в реализациях стандартной библиотеки. Но почему-то на это никто не жалуется. :) - person Serge Dundich; 22.06.2011
comment
Это неправильный ответ, потому что он отрицает возможность Boost. Вы можете использовать неопределенное поведение, если у вас достаточно опыта, как, например, у авторов Boost. - person 0kcats; 01.09.2016
comment
@0kcats: Ваш комментарий не имеет смысла в контексте вопроса. Вопрос в том, производит ли offsetof УБ или нет. Следует ли эксплуатировать неопределенное поведение — это совершенно другой вопрос. И ответ — нет, не следует, независимо от того, сколько опыта у вас есть. Вера в то, что авторы Boost используют UB, будет означать только то, что вы что-то неправильно истолковали/не поняли. - person AnT; 01.09.2016
comment
@AnT Скажем, у вас не было операции смещения в языке, но она была вам нужна. Определенно вы могли бы использовать тот же подход, что и в вопросе в Visual Studio. Или вы считаете, что это не сработает? Это указано в нижней части вашего ответа. Но если вы сделаете то же самое в своем коде (например, точно так же определите свой собственный макрос), это действительно приведет к неопределенному поведению, которое просто неправильно. - person 0kcats; 01.09.2016
comment
@0kcats: Компилятор может сказать, что если это выражение оценивается как результат макрорасширения offsetof, как определено заголовком stddef.h в год между 1980 и 3047, то результатом является size_t, представляющее смещение члена в пределах struct, в противном случае переформатируйте жесткий диск. Вот что означает неопределенное поведение: составители компилятора выбирают (явно или неявно) какое поведение. Итак, на этом компиляторе примера нет, вы не можете попробовать это сами. Настоящие компиляторы могут свободно документировать эти расширения, если хотят, но точно не хотят. - person GManNickG; 01.09.2016
comment
@0kcats: Нет. Современные компиляторы многое делают, чтобы воспользоваться преимуществами свободы оптимизации, предоставляемой UB. Во-первых, последствия этого в большинстве случаев чрезвычайно удивительны для сторонников философии undefined, которая действительно определяется реализацией. SO переполнен вопросами, связанными с этой темой. - person AnT; 02.09.2016
comment
Во-вторых, такие компиляторы часто содержат нестандартные внутренние функции, которые гарантируют, что любая полезная определенность UB доступна для кода стандартной библиотеки, но не [немедленно] доступна для пользовательского кода. Мое утверждение о том, что тот же самый код является UB в пользовательском коде (даже если он работает в стандартном заголовке), совершенно верно с этой точки зрения. - person AnT; 02.09.2016
comment
Хотя эти аргументы могут показаться логичными, они не соответствуют действительности. Заголовки составляются одинаково (приведите противоположный пример). Да, Undefined Behavior в языковом стандарте иногда, но далеко не всегда, является Undefined Behaviors конкретного компилятора на конкретной платформе. Не путайте это. Это всегда эксплуатируется. А также ошибки в компиляторах были обработаны разными странными способами. О, и если вы знаете, что в компиляторе есть ошибка, вы должны полностью прекратить ее использовать, потому что это UB любого кода, который вы пишете? Проверьте список ошибок в вашем любимом компиляторе и перестаньте писать код. - person 0kcats; 02.09.2016
comment
@0kcats: Да, эти аргументы реальны. Многие заголовки GCC, например, содержат различные директивы настройки, предназначенные для подавления предупреждений или адаптации другого поведения компилятора к специфичным для реализации функциям языка, используемым в этих стандартных заголовках. Эти корректировки отменяются до конца заголовка. - person AnT; 02.09.2016
comment
И даже если функции, специфичные для реализации, определяют что-то, что не определено языком, это не более чем расширение компилятора. К самому языку это не имеет никакого отношения. В области программирования C+ (или C) использование таких расширений без чрезвычайно веских и тщательно задокументированных причин является признаком некомпетентности. - person AnT; 02.09.2016
comment
Опять же, это очень просто: исходный код пользователя должен полностью соответствовать требованиям. Стандартная библиотека должна быть совместима функционально и только на уровне интерфейса. Утверждать, что эти требования в чем-то эквивалентны или даже сопоставимы, более чем смешно. Реализации могут свободно реализовать стандартную библиотеку на Фортране, если они того пожелают. - person AnT; 02.09.2016
comment
Точно такие же вещи, которые вы найдете в заголовках gcc, вы можете применить в заголовках вашей «частной» библиотеки, просто если они определены для этого единственного компилятора. Если код достаточно старый и нацелен на одну платформу, он будет пронизан языковыми UB, которые не являются UB для компилятора. То же самое верно, если вы ориентируетесь на несколько разных архитектур - в этом случае у вас будет набор собственных несовместимых заголовков с разными ifdef, которые работают с ошибками компилятора и предоставляют недостающую функциональность для разных компиляторов. - person 0kcats; 02.09.2016
comment
Код пользователя не обрабатывается иначе, чем другие заголовки. - person 0kcats; 02.09.2016

Когда стандарт C указывает, что определенные действия вызывают неопределенное поведение, это обычно не означает, что такие действия были запрещены, а скорее то, что реализации могут свободно указывать последующее поведение или нет по своему усмотрению. Следовательно, реализации могут свободно выполнять такие действия в тех случаях, когда Стандарт требует определенного поведения, если и только если реализации могут гарантировать, что поведение для этих действий будет соответствовать тому, что требует Стандарт. Рассмотрим, например, следующую реализацию strcpy:

char *strcpy(char *dest, char const *src)
{
  ptrdiff_t diff = dest-src-1;
  int ch;
  while((ch = *src++) != 0)
    src[diff] = ch;
  return dest;
}

Если src и dest являются несвязанными указателями, вычисление dest-src приведет к неопределенному поведению. Однако на некоторых платформах соотношение между char* и ptrdiff_t таково, что при любом char* p1, p2 вычисление p1 + (p2-p1); всегда будет равно p2. На платформах, которые дают такую ​​гарантию, приведенная выше реализация strcpy будет законной (и на некоторых таких платформах может быть быстрее, чем любая правдоподобная альтернатива). Однако на некоторых других платформах такая функция всегда может завершаться ошибкой, за исключением случаев, когда обе строки являются частью одного и того же выделенного объекта.

Тот же принцип применим к макросу offsetof. Не требуется, чтобы компиляторы предлагали какой-либо способ получить поведение, эквивалентное offsetof (кроме фактического использования этого макроса). тогда его макрос offsetof может это сделать. Если компилятор не поддержит никаких попыток использовать -> для чего-то другого, кроме допустимого указателя на экземпляр типа, тогда ему может потребоваться определить встроенную функцию, которая может вычислять смещение поля, и определить макрос offsetof для его использования. Важно не то, что Стандарт определяет поведение действий, выполняемых с использованием макросов и функций из стандартной библиотеки, а то, что реализация гарантирует, что поведение таких макросов и функций соответствует требованиям.

person supercat    schedule 09.08.2015

Это в основном эквивалентно вопросу, является ли это UB:

s* p = 0;
volatile auto& r = p->m;

Ясно, что доступ к памяти для цели r не генерируется, потому что это volatile, а компилятору запрещено генерировать ложные обращения к volatile переменным. Но *s не является изменчивым, поэтому компилятор может сгенерировать доступ к нему. Ни оператор адреса, ни приведение к ссылочному типу не создают неоцененный контекст в соответствии со стандартом.

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

Наконец, примечание в разделе [dcl.ref] говорит

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

person Ben Voigt    schedule 22.06.2011
comment
Любое значение указателя, которое используется для любых целей, всегда должно быть нулевым, указателем внутри допустимого объекта или указателем на пробел, следующий сразу за допустимым объектом. Любое действие, которое создает или использует rvalue указателя, не являющееся одним из перечисленных выше, вызовет Undefined Behavior, независимо от того, был ли когда-либо разыменован указатель. - person supercat; 09.08.2015

Это НЕ неопределенное поведение в C++, если m находится по смещению 0 в структуре s, а также в некоторых других случаях. Согласно проблеме 232 (выделено мной) :

Унарный оператор * выполняет косвенность: выражение, к которому он применяется, должно быть указателем на тип объекта или указателем на тип функции, а результатом является значение lvalue, относящееся к объекту или функции, на которую указывает выражение, если таковые имеются. . Если указатель является нулевым значением указателя (7.11 [conv.ptr]) или указывает на единицу после последнего элемента массива объекта (8.7 [expr.add]), результатом является пустое lvalue, не относящееся ни к какому объекту или функции. Пустое значение lvalue нельзя изменить.

Следовательно, &((s *)0)->m является неопределенным поведением, только если m не находится ни по смещению 0, ни по смещению, соответствующему адресу, который находится на единицу позже последнего элемента объекта массива. Обратите внимание, что добавление смещения 0 к null разрешено в C++, но не в C.

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

person personal_cloud    schedule 01.03.2019

Нет, это НЕ неопределенное поведение. Выражение разрешается во время выполнения.

Обратите внимание, что он берет адрес члена m из нулевого указателя. Это НЕ разыменование нулевого указателя.

person Richard Schneider    schedule 21.06.2011
comment
((s *)0)->m очевидно разыменовывает нулевой указатель. -> тоже разыменование. - person Xeo; 22.06.2011
comment
Выражение компилируется во время выполнения? - person Oliver Charlesworth; 22.06.2011
comment
Разве вещи не соблюдаются во время компиляции? - person genpfault; 22.06.2011
comment
@Richard Schneider: если он интерпретируется как простой код C/C++, он является разыменованием нулевого указателя, без вопросов. - person AnT; 22.06.2011
comment
@Xeo, @Andrey: в стандарте C99 прямо указано, что &*NULL эквивалентно NULL (я не знаю, говорит ли С++ что-то эквивалентное). Однако я еще не нашел ничего, что касалось бы этого конкретного случая. - person Oliver Charlesworth; 22.06.2011
comment
@OliCharlesworth Я не знаю, говорит ли С++ что-то эквивалентное). Я надеюсь, что С++ никогда не импортирует эту ерунду C! - person curiousguy; 03.11.2011
comment
Выражение разрешается во время выполнения, что ничего не значит. - person curiousguy; 03.11.2011
comment
@OliverCharlesworth Я почти уверен, что в стандарте C99 не это указано ни явно, ни неявно. Поправьте меня, если я ошибаюсь. - person yyny; 25.09.2016
comment
@curiousguy Чтобы быть педантичным, выражение разрешается во время выполнения значит что-то означает, а именно то, что выражение разрешается во время выполнения, однако, решение выражения во время выполнения в этом контексте означает, что выражение оценивается (т.е. вычисляется) во время выполнения. Однако это утверждение не обязательно верно, поскольку компиляторы обычно определяют ((*s)0)->m для возврата смещения члена m в структуре или объединении s, которое может и часто будет вычисляться во время компиляции. - person yyny; 25.09.2016
comment
@YoYoYonnY — см. сноску 87. - person Oliver Charlesworth; 25.09.2016
comment
@OliverCharlesworth Интересно! Я всегда думал, что gcc предупреждал об void *a = 0; &a[0];, потому что это было нестандартно, но я думаю, это просто потому, что gcc видит a[0] и ожидает, что он разыменует указатель void. - person yyny; 25.09.2016
comment
@OliverCharlesworth Сноски не являются нормативными. Указывает ли стандарт C, что иногда допускается разыменование нулевой точки? - person curiousguy; 25.09.2016
comment
@curiousguy - Вы говорите, что сноске нельзя доверять? - person Oliver Charlesworth; 25.09.2016
comment
@OliverCharlesworth На самом деле ничто в C std не заслуживает доверия! - person curiousguy; 25.09.2016