2016-05-24 2 views
10

Давайте предположим, у меня есть структура и извлечь смещение члена:Как использовать `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 в массиве).

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

+1

Я довольно уверен, ступенчатость к '' * полукокса и указатели, которые ссылаются на один и тот же объект (не обязательно массив) являются действительными. Однако, ожидая авторитетного ответа. – Quentin

+1

'(char *) base' можно использовать для перемещения в любом месте внутри' base' (и один за его конец). Любой объект ведет себя как массив размером 1. –

+0

'return * (int *) ((char *) base + off);' может с легкостью потерпеть неудачу, поскольку доступ 'int' может быть неровным. Например. Доступ 'int' может вызвать ошибку шины на нечетном адресе. OTOH OP действительно сказал: «Предположим ... у нас есть правильная структура A * и правильное смещение» – chux

ответ

3

Пер с C Standard, 7,19 Общие определения <stddef.h>, пункт 3, offsetof() определяется как:

макросы

NULL 

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

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

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

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

И 6.2.6.1 Общих, пункт 4 гласит:

Значение, хранящиеся в небитовом поле объектов любого другого типа объекта состоит из п × 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 значение составляет без знака.

+4

Указанные цитаты не имеют значения. Соответствующими частями стандарта будут те, что указаны в 6.5 относительно сглаживания указателей или, возможно, частей, относящихся к арифметике указателя. Я не вижу, как второй пример потерпит неудачу. 'intptr_t' - это целочисленный тип без знака, а не тип указателя. Он не выполняет никакой арифметики указателя, поэтому ваши предположения неверны. – Lundin

+0

@ Lundin - Да, ты прав. По какой-то причине я прочитал 'intptr_t' как' int * '. Пересмотр ответа теперь, но сначала мне нужно подумать о том, что произойдет, если 'intptr_t' будет подписан. –

+0

Согласитесь с @Lundin, кроме 'intptr_t', - это целочисленный тип со знаком vs.' uintptr_t' – chux

0

Причина, по которой стандарт (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), гласит, что:

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

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

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

+2

«Потому что intptr_t является целым числом, ... гарантированно существует ... компилятор (C99/C11)» -> «' intptr_t' ... 'uintptr_t' Эти типы являются _optional_." §7.20.1.4 1 – chux

+0

@chux Ах, тогда я узнал что-то новое! :) Изменит ответ, спасибо. – Lundin

Смежные вопросы