Как использовать `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
не нужно давать правильный результат (например, я вспоминаю, что некоторые CPU имеют возможность обрабатывать не байтовые выровненные адреса, что предполагает, что intptr_t
увеличивается с шагом 8 для каждого char
в массиве).
Похоже, что в стандарте есть что-то забытое (или то, что я пропустил).
2 ответа
Согласно стандарту 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
, 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
значение без знака.
Причина, по которой стандарт (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
если возможно.)
Следующий вопрос, если *(int*)((intptr_t)base + off)
хорошо определенное поведение. Часть стандарта, касающаяся преобразования указателей (6.3.2.3), гласит:
Любой тип указателя может быть преобразован в целочисленный тип. За исключением указанного ранее, результат определяется реализацией. Если результат не может быть представлен в целочисленном типе, поведение не определено. Результат не обязательно должен находиться в диапазоне значений любого целочисленного типа.
Для этого конкретного случая мы знаем, что у нас есть правильно выровненный int
там, так что все в порядке.
(Я не верю, что любые проблемы с наложением указателей применимы либо. По крайней мере, компиляция с gcc -O3 -fstrict-aliasing -Wstrict-aliasing=2
не нарушает код.)