Может ли макрос "container_of" быть строго соответствующим?

Обычно используемым макросом в ядре linux (и других местах) является container_of, который (в основном) определяется следующим образом:

#define container_of(ptr, type, member) (((type) *)((char *)(ptr) - offsetof((type), (member))))

Что в принципе позволяет восстановить "родительскую" структуру с указателем на один из его членов:

struct foo {
    char ch;
    int bar;
};
...
struct foo f = ...
int *ptr = &f.bar; // 'ptr' points to the 'bar' member of 'struct foo' inside 'f'
struct foo *g = container_of(ptr, struct foo, bar);
// now, 'g' should point to 'f', i.e. 'g == &f'

Однако не совсем ясно, считается ли вычитание, содержащееся в container_of undefined.

С одной стороны, поскольку bar внутри struct foo - это только одно целое число, то только *ptr должен быть действительным (а также ptr + 1). Таким образом, container_of эффективно создает выражение, подобное ptr - sizeof(int), что является undefined (даже без разыменования).

С другой стороны, в §6.3.2.3 с .7 из стандартных состояний С, которые преобразуют указатель на другой тип и обратно, должен указывать один и тот же указатель. Поэтому, "перемещая" указатель на середину объекта struct foo, а затем назад к началу должен выводить исходный указатель.

Основная проблема заключается в том, что реализациям разрешено проверять индексацию вне пределов времени во время выполнения. Моя интерпретация этого и вышеупомянутого требования эквивалентности указателя заключается в том, что границы должны быть сохранены во всех полях указателей (это включает в себя распад указателя - иначе, как вы могли бы использовать указатель на итерацию по массиву?). Ergo, а ptr может быть только указателем int, и ни ptr - 1, ни *(ptr + 1) не действительны, ptr должно все еще иметь некоторое представление о том, что находится в середине структуры, так что (char *)ptr - offsetof(struct foo, bar) (даже если на практике указатель равен ptr - 1).

Наконец, я натолкнулся на то, что если у вас есть что-то вроде:

int arr[5][5] = ...
int *p = &arr[0][0] + 5;
int *q = &arr[1][0];

в то время как поведение undefined для разыменования p, сам указатель действителен и должен сравниваться с q (см. этот вопрос), Это означает, что p и q сравнивают одно и то же, но могут быть разными в определенном порядке реализации (так что только q можно разыменовать). Это может означать следующее:

// assume same 'struct foo' and 'f' declarations
char *p = (char *)&f.bar;
char *q = (char *)&f + offsetof(struct foo, bar);

p и q сравнивают одно и то же, но могут иметь разные границы, связанные с ними, поскольку приведения к (char *) происходят от указателей к несовместимым типам.


Чтобы подвести итог, стандарт C не совсем ясен в отношении такого типа поведения, и попытка применить другие части стандарта (или, по крайней мере, мои интерпретации их) приводит к конфликтам. Итак, можно ли строго определить container_of? Если да, то правильное ли определение выше?


Это обсуждалось здесь после комментариев на мой ответ на этот вопрос.

Ответ 1

Я думаю, что это строго соответствует или есть большой дефект в стандарте. Обращаясь к вашему последнему примеру, раздел по арифметике указателя не дает компилятору какой-либо возможности относиться к p и q по-разному. Это не обусловлено тем, как было получено значение указателя, только то, на что он указывает.

Любая интерпретация, в которой p и q может обрабатываться по-разному в арифметике указателя, требует интерпретации, что p и q не указывают на один и тот же объект. Поскольку, поскольку не было зависимого от реализации поведения в том, как вы получили p и q, тогда это означало бы, что они не указывают на один и тот же объект при любой реализации. Это, в свою очередь, потребовало бы, чтобы p == q был ложным во всех реализациях, и поэтому все фактические реализации не соответствовали бы.

Ответ 2

Я просто хочу ответить на этот бит.

int arr[5][5] = ...
int *p = &arr[0][0] + 5;
int *q = &arr[1][0];

Это не UB. Несомненно, что p является указателем на элемент массива, при условии, что он находится в пределах границ. В каждом случае он указывает на 6-й элемент массива из 25 элементов и может быть безопасно разыменован. Он также может быть увеличен или уменьшен для доступа к другим элементам массива.

См. n3797 S8.3.4 для С++. Для C формулировка отличается, но смысл одинаков. В действительности массивы имеют стандартную компоновку и хорошо себя ведут в отношении указателей.


Предположим на мгновение, что это не так. Каковы последствия? Мы знаем, что макет массива int [5] [5] идентичен int [25], не может быть никакого дополнения, выравнивания или другой посторонней информации. Мы также знаем, что после образования p и q и получения значения они должны быть одинаковыми во всех отношениях.

Единственная возможность состоит в том, что если в стандарте указано, что UB и автор компилятора реализует стандарт, то достаточно бдительный компилятор может либо (a) выдать диагностику на основе анализа значений данных, либо (b) применить оптимизацию которая зависела от того, чтобы не отклоняться вне границ субмассив.

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