Как использовать `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); 
}

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

Другим подходом будет

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

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

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

Ответ 1

В Стандарт C, 7.19 Общие определения <stddef.h>, пункт 3, offsetof() определяется как

Макросы

NULL

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

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

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

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

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

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

Так как CHAR_BIT определяется как количество бит в char, a char - один байт.

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

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

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

Второй пример:

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

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

Ответ 2

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

На практике это будет работать, пока вы знаете, что делаете. 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, если это возможно. )

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

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

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

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