Макрос 'offsetof' извызвать неопределенное поведение? - PullRequest
18 голосов
/ 22 июня 2011

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

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

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

Ответы [ 6 ]

25 голосов
/ 22 июня 2011

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

(1) Является ли код UB по отношению к стандарту C ++?

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

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

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

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

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

Cheers & hth.,

15 голосов
/ 22 июня 2011

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

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

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

4 голосов
/ 09 августа 2015

Когда в стандарте 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 может сделать это.Если компилятор не будет поддерживать какие-либо попытки использовать -> для чего-либо, кроме допустимого указателя на экземпляр типа, тогда ему может потребоваться определить встроенную функцию, которая может вычислять смещение поля, и определить макрос offsetof дляиспользуйте это.Важно не то, чтобы Стандарт определял поведение действий, выполняемых с использованием макросов и функций стандартной библиотеки, а то, что реализация гарантирует, что поведение таких макросов и функций соответствует требованиям.

1 голос
/ 22 июня 2011

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

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

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

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

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

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

0 голосов
/ 01 марта 2019

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

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

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

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

0 голосов
/ 22 июня 2011

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

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

...