При вычислении выражение вида E1->E2 преобразуется в эквивалентную форму (*(E1)).E2. expr.ref / 2:
The expression E1->E2 is converted to the equivalent form (*(E1)).E2;
Получаем, что выражение ((S*)0)->m1 интерпретируется как выражение ( *((S*)0) ).m1. Т.е. здесь происходит разыменование нулевого указателя объектного типа, что является неопределённым поведением. Данное утверждение основано на следующих пунктах стандарта языка.
expr.unary.op / 1:
The unary * operator performs indirection: the expression to which it is applied shall be a pointer to an object type, or a pointer to a function type and the result is an lvalue referring to the object or function to which the expression points.
В результате разыменования указателя получается lvalue ссылающееся на объект, но нулевой указатель не указывает ни на какой объект.
dcl.ref / Note 2:
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 indirection through a null pointer, which causes undefined behavior.
Здесь явным образом говорится, что разыменование нулевого объектного указателя приводит к неопределённому поведению.
Связанные вопросы на enSO:
Таким образом поведение обоих выражений &(((S*)0)->m1) и (((S*)0)->m1) не определено, т.к. их вычисления требует разыменования нулевого объектного указателя. И при этом не важно, что компилятор в процессе оптимизации может избежать фактического обращения к «объекту», на который указывает указатель.
Стандарт языка не предъявляет требований к неопределённому поведению. Программа может успешно компилироваться и проявлять ожидаемое поведение, либо завершаться с ошибкой в процессе работы, либо может наблюдаться какое-либо иное поведение.
На самом деле ситуация с разыменованием указателя, не указывающего на объект, немного сложнее. Взглянем на следующий пример:
char a[10];
//Т.к. a[10] эквивалентно *(a+10), то UB - разыменовали указатель на гипотетический элемент за последним элементом массива.
char *b = &a[10];
Между этим примером и примером из вопроса
std::cout << &(((S*)0)->m1) << std::endl;
есть определённое сходство — фактически, значения «объектов», на которые указывают указатели не важны.
Здесь &a[10], нам не важно, какое значение находится за последним элементом массива — нам нужен указатель на элемент за последним элементом массива. Аналогично здесь &(((S*)0)->m1) нам не важно конкретное значение поля m1, нас интересует указатель.
Было бы неплохо доопределить поведение приведённых фрагментов кода в соответствии с интуитивными ожиданиями.
Более того в стандарте языка есть некоторая несогласованность. В описании оператора typeid определяется результат разыменования нулевого указателя. expr.typeid / 3:
When typeid is applied to a glvalue whose type is a polymorphic class type, the result refers to a std::type_info object representing the type of the most derived object (that is, the dynamic type) to which the glvalue refers. If the glvalue is obtained by applying the unary ``* operator to a pointer57 and the pointer is a null pointer value, the typeid expression throws an exception of a type that would match a handler of type std::bad_typeid exception.
Было предпринято несколько попыток легализовать разыменование указателя, не указывающего на объект в тех случаях, когда фактически не требуется доступ к значению.
В языке C операторы & и * в некоторых контекстах являются аннигилирующими по отношению друг к другу (см.: тонкости указателя на массив).
В языке C++ пытались ввести особую разновидность lvalue — empty lvalue, но эти попытки так и остались на стадии черновика (см.: Is indirection through a null pointer undefined behavior?).
Есть одна ситуация, когда стандарт языка требует довольно строгую проверку от реализаций на наличие неопределённого поведения в выражениях — это константные выражения.
Рассмотрим следующий код:
#include <iostream>
struct S
{
int m;
};
int main()
{
static S s;
constexpr const int* p = &(((S*) &s )->m);
std::cout << p;
}
Данный код успешно компилируется и выполняется (g++, clang).
Но если попытаться разыменовать нулевой указатель (даже если фактический доступ к значению не требуется), то получим ошибку компиляции.
constexpr const int* p = &(((S*) 0 )->m);
g++:
error: dereferencing a null pointer in '*0'
constexpr const int* p = &(((S*) 0 )->m);
clang:
error: constexpr variable 'p' must be initialized by a constant expression
constexpr const int* p = &(((S*) 0 )->m);
note: cannot access field of null pointer
constexpr const int* p = &(((S*) 0 )->m);
offsetof– Barracudach Dec 25 '21 at 11:59&(((S*)0)->m1)зависит от того, что находится в памяти((S*)0)->m1? Эдак вы скажете, что вint n = 5; int * p = &n;адрес в переменнойpзависит от значения в переменнойn... – Harry Dec 25 '21 at 12:20offsetofэто все-таки специальная конструкция, определенная стандартом. Так что не стоит надеятся, что похожий собственный код будет работать так же. Тем более, что обычно используется специальный встроенный оператор. – user7860670 Dec 25 '21 at 14:54