Принимает ли адрес переменной-члена через нулевой указатель значение undefined?

Следующий код (или его эквивалент, который использует явные приведения нулевого литерала для избавления от временной переменной) часто используется для вычисления смещения определенной переменной-члена в классе или структуре:

class Class {
public:
    int first;
    int second;
};

Class* ptr = 0;
size_t offset = reinterpret_cast<char*>(&ptr->second) -
                 reinterpret_cast<char*>(ptr);

&ptr->second выглядит следующим образом:

&(ptr->second)

что, в свою очередь, эквивалентно

&((*ptr).second)

который разделяет указатель экземпляра объекта и дает поведение undefined для нулевых указателей.

Итак, это оригинальный штраф или он дает UB?

Ответ 1

Несмотря на то, что он ничего не делает, char* foo = 0; *foo; является может быть undefined.

Вызов нулевого указателя может быть undefined. И да, ptr->foo эквивалентно (*ptr).foo, а *ptr вызывает нулевой указатель.

В рабочих группах сейчас открытая проблема, если поведение *(char*)0 undefined, если вы его не читаете или не пишете. Части стандарта подразумевают это, другие части подразумевают, что это не так. Текущие заметки, похоже, склоняются к тому, чтобы определить его.

Теперь, это теоретически. Как на практике?

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

Вот почему ссылка cpp offsetof перечисляет в значительной степени этот трюк как возможную реализацию. Тот факт, что некоторые (многие? Большинство? Каждый, кого я проверил?), Компиляторы реализуют offsetof аналогичным или эквивалентным образом, не означает, что поведение хорошо определено в стандарте С++.

Однако, учитывая неоднозначность, компиляторы могут свободно добавлять проверки в каждую инструкцию, которая разделяет указатель, и выполнять произвольный код (например, при сбое быстрого сообщения об ошибках), если null действительно разыменован. Такая аппаратура может быть даже полезной для поиска ошибок, где они происходят, а не там, где происходит симптом. И в системах, где имеется доступная для записи память около 0, такая аппаратура может быть ключевой (у pre OSX MacOS было некоторое количество записываемой памяти, которая контролировала системные функции около 0).

Такие компиляторы могли бы написать offsetof таким образом и ввести pragma или тому подобное, чтобы заблокировать инструментарий в сгенерированном коде. Или они могут переключиться на внутреннюю.

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

Наконец, некоторые компиляторы имеют агрессивные параметры оптимизации, которые находят код, который выполняет поведение undefined (по крайней мере, в определенных ветвях или условиях) и использует это, чтобы отметить эту ветвь как недостижимую. Если будет принято решение о том, что нулевое разыменование является undefined, это может быть проблемой. Классическим примером является gcc-атакующий выпрямитель целых чисел с переполнением. Если стандарт диктует что-то, это поведение undefined, компилятор может считать эту ветвь недостижимой. Если нулевое разыменование не находится за веткой в ​​функции, компилятор может объявить весь код, который вызывает эту функцию, недоступен и рекурсивно.

И было бы свободно делать это не в текущей, а в следующей версии вашего компилятора.

Написание кода, который является стандартным, - это не просто код, который сегодня компилируется. Хотя степень разыменования и не использующая нулевой указатель в настоящее время неоднозначна, полагаясь на то, что только двусмысленно определено, является рискованным.