Как использовать `offsetof` для доступа к полю стандартным образом?

Предположим, у меня есть структура и извлечение смещения для члена:

struct A { int x; }; size_t xoff = offsetof(A, x); 

как я могу, указав указатель на struct A вытащить элемент стандартным образом? Предполагая, конечно, что у нас есть правильная struct A* и правильное смещение. Одна попытка – сделать что-то вроде:

 int getint(struct A* base, size_t off) { return *(int*)((char*)base + off); } 

Что, вероятно, сработает, но обратите внимание, например, что указатель-арифметика только кажется определенным в стандарте, если указатели являются указателями одного и того же массива (или одного конца), это не обязательно. Так что технически эта конструкция, похоже, полагается на неопределенное поведение.

Другой подход

 int getint(struct A* base, size_t off) { return *(int*)((uintptr_t)base + off); } 

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

Похоже, что в стандарте что-то забыто (или что-то я пропустил).

По стандарту C , 7.19. Общие определения , пункт 3, offsetof() определяется как:

Макросы

 NULL 

который расширяется до константы нулевого указателя, определяемой реализацией; а также

 offsetof(*type*, *member-designator*) 

который расширяется до целочисленного константного выражения, которое имеет тип size_t , значение которого является смещением в байтах, члену структуры (обозначенному обозначением- членом ), с начала его структуры (обозначается типом ).

Таким образом, offsetoff() возвращает смещение в байтах .

И 6.2.6.1 Общие положения , пункт 4 гласит:

Значения, хранящиеся в объектах без битового поля любого другого типа объекта, состоят из бит n × CHAR_BIT , где n – размер объекта этого типа, в байтах.

Поскольку CHAR_BIT определяется как количество бит в char , char – один байт .

Итак, это правильно, по стандарту:

 int getint(struct A* base, size_t off) { return *(int*)((char*)base + off); } 

Это преобразует base в char * и добавляет байты в адрес. Если off – результат offsetof(A, x); , результирующий адрес – это адрес x в structure A которую указывает точка.

Ваш второй пример:

 int getint(struct A* base, size_t off) { return *(int*)((intptr_t)base + off); } 

зависит от результата добавления подписанного значения intptr_t с неподписанным значением size_t без знака.

Причина, по которой стандарт (6.5.6) допускает только арифметику указателей для массивов, заключается в том, что структуры могут иметь байты заполнения для удовлетворения требований выравнивания. Таким образом, арифметика указателя внутри структуры действительно является формально неопределенным поведением.

На практике это будет работать, пока вы знаете, что делаете. base + off не может потерпеть неудачу, потому что мы знаем, что там есть достоверные данные, и он не смещен, учитывая, что к нему обращаются должным образом.

Поэтому (intptr_t)base + off действительно намного лучший код, так как больше нет никакой арифметики указателя, а просто простая целочисленная арифметика. Поскольку intptr_t является целым числом, это не указатель.

Как указано в комментарии, этот тип не гарантированно существует, он является необязательным согласно 7.20.1.4/1. Я полагаю, что для максимальной переносимости вы можете переключиться на другие типы, которые гарантированно существуют, например intmax_t или ptrdiff_t . Однако можно утверждать, что компилятор C99 / C11 без поддержки intptr_t вообще полезен.

(Здесь проблема с небольшим типом, а именно, что intptr_t является подписанным типом и не обязательно совместим с size_t . Вы можете получить неявные проблемы с продвижением по типу. Безопаснее использовать uintptr_t если это возможно.)

Следующий вопрос: if *(int*)((intptr_t)base + off) является корректным поведением. Часть стандарта, касающегося конверсий указателей (6.3.2.3), гласит, что:

Любой тип указателя может быть преобразован в целочисленный тип. За исключением случаев, указанных ранее, результат определяется реализацией. Если результат не может быть представлен в целочисленном типе, поведение не определено. Результат не обязательно должен находиться в диапазоне значений любого целочисленного типа.

В этом конкретном случае мы знаем, что мы правильно выровняли int , так что это нормально.

(Я не считаю, что любые проблемы с псевдонимом указателей применяются также. По крайней мере, компиляция с gcc -O3 -fstrict-aliasing -Wstrict-aliasing=2 не нарушает код.)