Расширение структуры в C
Недавно я встретил код коллеги, который выглядел так:
typedef struct A {
int x;
}A;
typedef struct B {
A a;
int d;
}B;
void fn(){
B *b;
((A*)b)->x = 10;
}
Его объяснение состояло в том, что поскольку struct A
был первым членом struct B
, поэтому b->x
будет таким же, как b->a.x
, и обеспечивает лучшую читаемость.
Это имеет смысл, но считается ли это хорошей практикой? И будет ли это работать на разных платформах? В настоящее время это отлично работает на GCC.
Ответы
Ответ 1
Да, он будет работать кросс-платформенный, но это не обязательно делает его хорошей идеей.
В соответствии со стандартом ISO C (все цитаты из C11), 6.7.2.1 Structure and union specifiers /15
, перед первым элементом структуры
не допускается заполнение,
Кроме того, 6.2.7 Compatible type and composite type
утверждает, что:
Два типа имеют совместимый тип, если их типы одинаковы
и неоспоримо, что типы A
и A-within-B
идентичны.
Это означает, что память, обращаясь к полям A
, будет одинаковой для типов A
и B
, так же как и более разумный b->a.x
, который, вероятно, будет использоваться, если у вас есть опасения относительно ремонтопригодности в будущем.
И, хотя вам обычно приходится беспокоиться о строгом псевдониме типов, я не считаю, что это применимо здесь. Это незаконно для указателей псевдонимов, но стандарт имеет особые исключения.
6.5 Expressions /7
содержит некоторые из этих исключений со сноской:
Цель этого списка - указать те обстоятельства, при которых объект может или не может быть сглажен.
Перечисленные исключения:
-
a type compatible with the effective type of the object
;
- некоторые другие исключения, которые нас здесь не интересуют; и
-
an aggregate or union type that includes one of the aforementioned types among its members (including, recursively, a member of a subaggregate or contained union)
.
Это, в сочетании с упомянутыми выше правилами прокладки структуры, включая фразу:
Указатель на объект структуры, соответствующим образом преобразованный, указывает на его начальный член
похоже, что этот пример специально разрешен. Здесь мы должны помнить, что тип выражения ((A*)b)
равен A*
, а не B*
. Это делает переменные совместимыми для неограниченного сглаживания.
Чтобы мое чтение соответствующих частей стандарта, я был не прав перед (a) но в этом случае я сомневаюсь.
Итак, если у вас есть настоящая потребность в этом, все будет хорошо, но я буду документировать любые ограничения в коде, очень близком к структурам, чтобы не укусить в будущем.
(a) Как моя жена расскажет вам, часто и без особых подсказок: -)
Ответ 2
Я выйду на конечность и буду против @paxdiablo: я думаю, что это прекрасная идея, и это очень распространено в крупном производственном качестве код.
Это в основном самый очевидный и приятный способ реализации объектно-ориентированных структур данных на основе наследования. C. Запуск объявления struct B
с экземпляром struct A
означает, что "B - подкласс класса A". Тот факт, что первый член структуры гарантированно составляет 0 байт от начала структуры, - это то, что заставляет его работать безопасно, и, по моему мнению, он красив на грани.
Он широко используется и развертывается в коде на основе библиотеки GObject, такой как набор инструментов пользовательского интерфейса GTK + и среда рабочего стола GNOME.
Конечно, это требует, чтобы вы "знали, что делаете", но обычно это всегда происходит при реализации сложных отношений типа в C.:)
В случае GObject и GTK + есть много инфраструктуры поддержки и документации, которые помогут в этом: довольно сложно забыть об этом. Это может означать, что создание нового класса - это не то, что вы делаете так же быстро, как на С++, но этого, возможно, следует ожидать, поскольку в классах C нет встроенной поддержки.
Ответ 3
Все, что обходит проверку типов, обычно следует избегать.
Этот хак полагается на порядок объявлений, и ни компилятор, ни этот порядок не могут быть применены компилятором.
Он должен работать кросс-платформенным, но я не думаю, что это хорошая практика.
Если у вас действительно есть глубоко вложенные структуры (возможно, вам стоит задаться вопросом, почему, однако), тогда вы должны использовать временную локальную переменную для доступа к полям:
A deep_a = e->d.c.b.a;
deep_a.x = 10;
deep_a.y = deep_a.x + 72;
e->d.c.b.a = deep_a;
Или, если вы не хотите копировать a
вдоль:
A* deep_a = &(e->d.c.b.a);
deep_a->x = 10;
deep_a->y = deep_a->x + 72;
Это показывает, откуда приходит a
, и для него не требуется бросок.
Java и С# также регулярно выставляют такие конструкции, как "c.b.a", я не понимаю, в чем проблема. Если то, что вы хотите имитировать, является объектно-ориентированным поведением, тогда вы должны рассмотреть использование объектно-ориентированного языка (например, С++), поскольку "расширение структур" в том, как вы предлагаете, не обеспечивает инкапсуляцию и полиморфизм во время выполнения (хотя можно утверждать что ((A *) b) сродни "динамическому приведению" ).
Ответ 4
Это ужасная идея. Как только кто-то приходит и вставляет другое поле в передней части структуры B, ваша программа взрывается. И что не так с b.a.x
?
Ответ 5
Я сожалею о том, что не согласен со всеми другими ответами здесь, но эта система не соответствует стандарту C. Недопустимо иметь два указателя с разными типами, которые указывают на одно и то же место в одно и то же время, это называется aliasing и не допускается правилами строгого сглаживания в C99 и многими другими стандартами. Менее уродливым было сделать это, чтобы использовать встроенные функции getter, которые тогда не должны выглядеть аккуратно. Или, возможно, это работа для профсоюза? В частности, разрешено удерживать один из нескольких типов, однако есть и множество других недостатков.
Короче говоря, такой грязный кастинг для создания полиморфизма не допускается большинством стандартов С только потому, что он, похоже, работает на вашем компиляторе, это не значит, что это приемлемо. См. Здесь, чтобы объяснить, почему это недопустимо, и почему компиляторы на высоких уровнях оптимизации могут нарушать код, который не соответствует этим правилам http://en.wikipedia.org/wiki/Aliasing_%28computing%29#Conflicts_with_optimization
Ответ 6
Да, это сработает. И это один из основных принципов Object Oriented using C. См. Этот ответ Объектная ориентация в C" для получения дополнительных примеров расширения (например, наследования).
Ответ 7
Это совершенно законно и, на мой взгляд, довольно элегантно. Пример этого в производственном коде см. В Документы GObject:
Благодаря этим простым условиям можно определить тип каждого экземпляра объекта:
B *b;
b->parent.parent.g_class->g_type
или, быстрее:
B *b;
((GTypeInstance*)b)->g_class->g_type
Лично я считаю, что профсоюзы уродливы и имеют тенденцию приводить к огромным операторам switch
, что является большой частью того, что вы пытались избежать, написав OO-код. В этом стиле я пишу значительную часть кода. Обычно первый член struct
содержит указатели на функции, которые можно заставить работать как vtable для рассматриваемого типа.
Ответ 8
Я вижу, как это работает, но я бы не назвал эту хорошую практику. Это зависит от того, как байты каждой структуры данных помещаются в память. Каждый раз, когда вы бросаете одну сложную структуру данных в другую (т.е. Struct), это не очень хорошая идея, особенно если две структуры не имеют одинакового размера.
Ответ 9
Я думаю, что OP и многие комментаторы заперли идею о том, что код расширяет структуру.
Это не так.
Это и пример композиции. Очень полезно. (Избавьтесь от typedefs, вот более описательный пример):
struct person {
char name[MAX_STRING + 1];
char address[MAX_STRING + 1];
}
struct item {
int x;
};
struct accessory {
int y;
};
/* fixed size memory buffer.
The Linux kernel is full of embedded structs like this
*/
struct order {
struct person customer;
struct item items[MAX_ITEMS];
struct accessory accessories[MAX_ACCESSORIES];
};
void fn(struct order *the_order){
memcpy(the_order->customer.name, DEFAULT_NAME, sizeof(DEFAULT_NAME));
}
У вас есть буфер фиксированного размера, который прекрасно разделен. Он уверен, превосходит гигантскую структуру одного уровня.
struct double_order {
struct order order;
struct item extra_items[MAX_ITEMS];
struct accessory extra_accessories[MAX_ACCESSORIES];
};
Итак, теперь у вас есть вторая структура, которая может быть обработана (а-наследование) точно так же, как и первая с явным литом.
struct double_order d;
fn((order *)&d);
Это сохраняет совместимость с кодом, который был написан для работы с меньшей структурой. Ядро Linux (http://lxr.free-electrons.com/source/include/linux/spi/spi.h (посмотрите на struct spi_device)) и библиотеку bsd сокетов (http://beej.us/guide/bgnet/output/html/multipage/sockaddr_inman.html) используют этот подход. В случаях с ядром и сокетами у вас есть структура, которая запускается как в общих, так и в дифференцированных разделах кода. Не все, что отличается от варианта использования для наследования.
Я бы не предлагал писать такие структуры просто для удобства чтения.
Ответ 10
Я думаю, Postgres делает это и в некоторых своих кодах. Не то, чтобы это делало это хорошей идеей, но оно действительно говорит о том, насколько широко это принято.
Ответ 11
Возможно, вы можете использовать макросы для реализации этой функции, необходимость повторного использования функции или поля в макросе.