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

11 60

11 ответов:

Да, он будет работать кросс-платформенный, но это не обязательно очень хорошая идея.

согласно стандарту 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) как скажет моя жена вы, часто и без особых подсказок :-)

Я выйду на конечности и против @paxdiablo на этом: я думаю, что это прекрасная идея, и это очень распространено в большом, производственном коде.

это в основном самый очевидный и приятный способ реализации объектно-ориентированных структур данных на основе наследования В C. Начиная объявление struct B с struct A означает "B является подклассом A". Тот факт, что первый член структуры гарантированно будет 0 байт от начала структуры что заставляет его работать безопасно, и это погранично красиво, на мой взгляд.

Он широко используется и развертывается в коде на основе GObject библиотека, такая как инструментарий пользовательского интерфейса GTK+ и среда рабочего стола GNOME.

конечно, это требует от вас "знать, что вы делаете", но это, как правило, всегда имеет место при реализации сложных отношений типа В C. :)

в случае GObject и GTK+, есть много поддержки инфраструктуры и документации, чтобы помочь с этим: это довольно трудно забыть об этом. Это может означать, что создание нового класса-это не то, что вы делаете так же быстро, как в C++, но это, возможно, следует ожидать, поскольку в C нет собственной поддержки для классов.

все, что обходит проверку типа, как правило, следует избегать. Этот хак полагается на порядок объявлений, и ни приведение, ни этот порядок не могут быть применены компилятором.

Он должен работать кросс-платформенный, но я не думаю, что это хорошая практика.

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

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# также регулярно выставляют конструкции, такие как" c.b.a", я не вижу, в чем проблема. Если то, что вы хотите имитировать, является объектно-ориентированным поведением, тогда вам следует рассмотреть возможность использования объектно-ориентированного языка( например, C++), поскольку "расширение структур" в том, как вы предлагаете, не обеспечивает инкапсуляции или полиморфизма времени выполнения (хотя можно спорить что ((А*)Б) похож на "приведение").

Это ужасная идея. Как только кто-то приходит и вставляет другое поле в передней части структуры B, ваша программа взрывается. И что такого плохого в b.a.x?

Я сожалею, что не согласен со всеми другими ответами здесь, но эта система не соответствует стандарту C. недопустимо иметь два указателя с разными типами, которые указывают на одно и то же место одновременно, это называется сглаживанием и не допускается строгими правилами сглаживания в C99 и многих других стандартах. Менее уродливым было бы сделать это, чтобы использовать встроенные функции геттера, которые затем не должны выглядеть аккуратно таким образом. Или, может быть, это работа для профсоюза? Специально разрешено проводить один из нескольких типов, однако есть множество других недостатков там тоже.

короче говоря, такой грязный кастинг для создания полиморфизма не допускается большинством стандартов C, просто потому, что он, похоже, работает на вашем компиляторе, не означает, что это приемлемо. См. здесь объяснение того, почему это не разрешено, и почему компиляторы на высоких уровнях оптимизации могут нарушать код, который не следует этим правилам http://en.wikipedia.org/wiki/Aliasing_%28computing%29#Conflicts_with_optimization

Да, это будет работать. И это один из основных принципов объектно-ориентированный с помощью C. Смотрите этот ответ'объектная ориентация в C ' для получения дополнительных примеров расширения (т. е. наследования).

это совершенно законно, и, на мой взгляд, весьма элегантно. Пример этого в производственном коде см. В разделе содержащих документы:

благодаря этим простым условиям можно определить тип каждого экземпляра объекта, выполнив:

B *b;
b->parent.parent.g_class->g_type

или, быстрее:

B *b;
((GTypeInstance*)b)->g_class->g_type

лично я считаю, что профсоюзы уродливы и имеют тенденцию вести к огромным switch заявления, которые являются большой частью того, что у вас есть работал, чтобы избежать, написав OO-код. Я пишу значительное количество кода сам в этом стиле - - - как правило, первый член struct содержит указатели на функции, которые могут работать как vtable для рассматриваемого типа.

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

Я думаю, что 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) используйте этот подход. В случаях ядра и сокетов у вас есть структура, которая является выполните как общие, так и дифференцированные разделы кода. Не все, что отличается от варианта использования для наследования.

Я бы не предложил писать такие структуры только для удобства чтения.

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

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