Вызывает ли макрос 'offsetof' из <stddef.h> поведение undefined?

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

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

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

Ответ 1

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

(1) Является ли код UB относительно стандарта С++?

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

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

(2) Является ли код UB относительно компилятора, который он написал для?

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

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

Приветствия и hth.,

Ответ 2

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

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

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

Ответ 3

Когда в стандарте C указано, что определенные действия вызывают Undefined Behavior, это вообще не означает, что такие действия были запрещены, а скорее, что реализации могли свободно указывать последующие поведения или нет по своему усмотрению. Следовательно, реализации могли бы свободно выполнять такие действия в случаях, когда Стандарт требует определенного поведения, тогда и только тогда, когда реализации могут гарантировать, что поведение этих действий будет соответствовать тому, что требуется Стандарту. Рассмотрим, например, следующую реализацию 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 приведет к Undefined Поведению. Однако на некоторых платформах отношение между char* и ptrdiff_t таково, что при любом char* p1, p2 вычисление p1 + (p2-p1); всегда будет равно p2. На платформах, которые делают эту гарантию, вышеуказанная реализация strcpy была бы законной (и на некоторых таких платформах могла бы быть быстрее любой возможной альтернативы). Однако на некоторых других платформах такая функция может всегда терпеть неудачу, за исключением случаев, когда обе строки являются частью одного и того же выделенного объекта.

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

Ответ 4

Это в основном эквивалентно заданию, является ли это UB:

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

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

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

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

в частности, нулевая ссылка не может существовать в хорошо определенной программе, так как единственным способом создания такой ссылки было бы привязать ее к "объекту", полученному путем разыменования нулевого указателя, что вызывает поведение undefined.

Ответ 5

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

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