Расширение структуры в 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

Возможно, вы можете использовать макросы для реализации этой функции, необходимость повторного использования функции или поля в макросе.