Как использовать `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
не нарушает код.)