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

Другие вопросы по тегам