Een struct uitbreiden in C

Ik kwam onlangs de code van een collega tegen die er als volgt uitzag:

typedef struct A {
  int x;
}A;
typedef struct B {
  A a;
  int d;
}B;
void fn(){
  B *b;
  ((A*)b)->x = 10;
}

Zijn uitleg was dat aangezien struct Ahet eerste lid was van struct B, dus b->xhetzelfde zou zijn als b->a.xen zorgt voor een betere leesbaarheid.
Dit is logisch, maar wordt dit als een goede praktijk beschouwd? En werkt dit op verschillende platforms? Momenteel werkt dit prima op GCC.


Antwoord 1, autoriteit 100%

Ja, het werkt platformonafhankelijk(a), maar dat maakt het niet noodzakelijkerwijseen goed idee.

Volgens de ISO C-standaard (alle citaten hieronder zijn afkomstig uit C11), 6.7.2.1 Structure and union specifiers /15, is het niet toegestaan ​​om voorop te vullen het eerste element van een structuur

Bovendien stelt 6.2.7 Compatible type and composite typedat:

Twee typen hebben een compatibel type als hun typen hetzelfde zijn

en het staat buiten kijf dat de typen aen A-within-Bidentiek zijn.

Dit betekent dat de geheugentoegangen tot de a-velden hetzelfde zullen zijn in zowel het type aals b, wat logischer zou zijn b->a.xdat is waarschijnlijk wat u zouzou moeten gebruiken als u zich zorgen maakt over onderhoudbaarheid in de toekomst.

En hoewel u zich normaal gesproken zorgen zou moeten maken over strikte type-aliasing, geloof ik niet dat dat hier van toepassing is. Het isillegaal om aliassen aan te wijzen, maar de standaard heeft specifieke uitzonderingen.

6.5 Expressions /7vermeldt enkele van die uitzonderingen, met de voetnoot:

De bedoeling van deze lijst is om de omstandigheden te specificeren waarin een object al dan niet een alias mag hebben.

De vermelde uitzonderingen zijn:

  • a type compatible with the effective type of the object;
  • enkele andere uitzonderingen die ons hier niet hoeven te interesseren; en
  • 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).

Dat, gecombineerd met de hierboven genoemde struct-padding-regels, inclusief de zin:

Een aanwijzer naar een structuurobject, op de juiste manier geconverteerd, wijst naar zijn oorspronkelijke lid

lijkt aan te geven dat dit voorbeeld specifiek is toegestaan. Het belangrijkste punt dat we hier moeten onthouden, is dat het type van de uitdrukking ((A*)b)A*is, niet B*. Dat maakt de variabelen compatibel voor onbeperkte aliasing.

Dat is mijn lezing van de relevante delen van de norm, ik heb het eerder bij het verkeerde eind gehad (b), maar in dit geval betwijfel ik dat.

Dus, als je hier een echtebehoefte aan hebt, zal het goed werken, maar ik zou eventuele beperkingen in de code zeerdicht bij de structuren documenteren, zodat om in de toekomst niet gebeten te worden.


(a)In algemene zin. Natuurlijk, het codefragment:

B *b;
((A*)b)->x = 10;

zal ongedefinieerd gedrag zijn omdat bniet is geïnitialiseerd op iets zinnigs. Maar ik ga ervan uit dat dit slechts een voorbeeldcode is die bedoeld is om uw vraag te illustreren. Als iemand zich er zorgen over maakt, beschouw het dan als:

B b, *pb = &b;
((A*)pb)->x = 10;

(b)Zoals mijn vrouw je zal vertellen, vaak en met weinig aansporing 🙂


Antwoord 2, autoriteit 52%

Ik zal op mijn hoede zijn en me verzetten tegen @paxdiabloover deze: ik vind het een goed idee, en het is heel gebruikelijk in grote code van productiekwaliteit.

Het is in feite de meest voor de hand liggende en leuke manier om op overerving gebaseerde objectgeoriënteerde datastructuren in C te implementeren. Het starten van de declaratie van struct Bmet een instantie van struct Abetekent “B is een subklasse van A”. Het feit dat het eerste structuurlid vanaf het begin van de structuur gegarandeerd 0 bytes is, zorgt ervoor dat het veilig werkt, en het is naar mijn mening prachtig.

Het wordt veel gebruikt en geïmplementeerd in code op basis van de GObject-bibliotheek, zoals de GTK+-toolkit voor gebruikersinterface en de GNOME-desktopomgeving.

Natuurlijk moet je “weten wat je doet”, maar dat is over het algemeen altijd het geval bij het implementeren van gecompliceerde typerelaties in C. 🙂

In het geval van GObject en GTK+ is er voldoende ondersteunende infrastructuur en documentatie om hierbij te helpen: het is best moeilijk om het te vergeten. Het kan betekenen dat het maken van een nieuwe klasse niet iets is dat je even snel doet als in C++, maar dat is misschien te verwachten aangezien er geen native ondersteuning in C is voor klassen.


Antwoord 3, autoriteit 18%

Dat is een verschrikkelijk idee. Zodra iemand langskomt en een ander veld aan de voorkant van struct B invoegt, ontploft je programma. En wat is er zo mis met b.a.x?


Antwoord 4, autoriteit 18%

Alles dat typecontrole omzeilt, moet over het algemeen worden vermeden.
Deze hack is afhankelijk van de volgorde van de verklaringen en noch de cast, noch deze volgorde kan worden afgedwongen door de compiler.

Het zou platformonafhankelijk moeten werken, maar ik denk niet dat het een goede gewoonte is.

Als je echt diep geneste structuren hebt (je moet je misschien afvragen waarom), dan zou je een tijdelijke lokale variabele moeten gebruiken om toegang te krijgen tot de velden:

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;

Of, als je aniet mee wilt kopiëren:

A* deep_a = &(e->d.c.b.a);
deep_a->x = 10;
deep_a->y = deep_a->x + 72;

Dit laat zien waar avandaan komt en het vereist geen cast.

Java en C# stellen ook regelmatig constructies zoals “c.b.a” bloot, ik zie niet in wat het probleem is. Als u objectgeoriënteerd gedrag wilt simuleren, moet u overwegen een objectgeoriënteerde taal (zoals C++) te gebruiken, aangezien “structs uitbreiden” op de manier die u voorstelt geen inkapseling of runtime-polymorfisme biedt (hoewel men kan beweren dat ((A*)b) verwant is aan een “dynamische cast”).


Antwoord 5, autoriteit 11%

Het spijt me dat ik het niet eens ben met alle andere antwoorden hier, maar dit systeem voldoet niet aan standaard C. Het is niet acceptabel om twee wijzers van verschillende typen te hebben die tegelijkertijd naar dezelfde locatie wijzen, dit heet aliasing en is niet toegestaan ​​door de strikte aliasingregels in C99 en vele andere normen. Een minder lelijke was om dit te doen zou zijn om in-line getter-functies te gebruiken die er dan niet zo netjes uit hoeven te zien. Of misschien is dit de baan voor een vakbond? Specifiek toegestaan ​​om een ​​van de verschillende typen vast te houden, maar er zijn ook talloze andere nadelen.

Kortom, dit soort vuile casting om polymorfisme te creëren is volgens de meeste C-normen niet toegestaan, alleen omdat het lijkt te werken op je compiler, wil nog niet zeggen dat het acceptabel is. Zie hier voor een uitleg waarom het niet is toegestaan ​​en waarom compilers met een hoog optimalisatieniveau code kunnen breken die niet aan deze regels voldoet http://en.wikipedia.org/wiki/Aliasing_%28computing%29#Conflicts_with_optimization


Antwoord 6, autoriteit 8%

Ja, het zal werken. En het is een van de kernprincipes van Object Oriented using C. Zie dit antwoord ‘Object-orientation in C‘ voor meer voorbeelden over verlengen ( dwz erfenis).


Antwoord 7, autoriteit 5%

Dit is volkomen legaal en naar mijn mening behoorlijk elegant. Zie voor een voorbeeld hiervan in productiecode de GObject-documenten:

Dankzij deze eenvoudige voorwaarden is het mogelijk om het type te detecteren
van elke objectinstantie door te doen:

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

of, sneller:

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

Persoonlijk denk ik dat vakbonden lelijk zijn en leiden tot enorme switch-statements, wat een groot deel is van wat je hebt proberen te vermijden door OO-code te schrijven. Ik schrijf zelf een aanzienlijke hoeveelheid code in deze stijl — meestal bevat het eerste lid van de structfunctieaanwijzers die kunnen worden gemaakt om te werken als een vtable voor het betreffende type.


Antwoord 8, autoriteit 3%

Ik kan zien hoe dit werkt, maar ik zou dit geen goede praktijk noemen. Dit is afhankelijk van hoe de bytes van elke datastructuur in het geheugen worden geplaatst. Elke keer dat u de ene gecompliceerde datastructuur naar de andere cast (bijv. structs), is dat geen goed idee, vooral niet wanneer de twee structuren niet even groot zijn.


Antwoord 9, autoriteit 2%

Ik denk dat de OP en veel commentatoren hebben vastgehouden aan het idee dat de code een structuur uitbreidt.

Dat is het niet.

Dit is een voorbeeld van compositie. Erg nuttig. (Het wegwerken van de typedefs, hier is een meer beschrijvend voorbeeld):

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));
}

Je hebt een buffer met een vaste grootte die mooi is gecompartimenteerd. Het verslaat zeker een gigantische structuur met één laag.

struct double_order {
  struct order order;
  struct item extra_items[MAX_ITEMS];
  struct accessory extra_accessories[MAX_ACCESSORIES];
};

Dus nu heb je een tweede struct die kan worden behandeld (a la overerving) precies zoals de eerste met een expliciete cast.

struct double_order d;
fn((order *)&d);

Hierdoor blijft de compatibiliteit behouden met code die is geschreven om met de kleinere structuur te werken. Zowel de Linux-kernel (http://lxr.free-electrons. com/source/include/linux/spi/spi.h(kijk naar struct spi_device)) en bsd sockets-bibliotheek (http://beej.us/guide/bgnet/output/html/multipage/sockaddr_inman.html) gebruiken deze benadering. In de kernel- en sockets-gevallen heb je een struct die door zowel generieke als gedifferentieerde codesecties wordt geleid. Niet zo heel anders dan de use case voor overerving.

Ik zou NIET aanraden om dergelijke constructies te schrijven alleen voor de leesbaarheid.


Antwoord 10

Ik denk dat Postgres dit ook in sommige van hun code doet. Niet dat het een goed idee is, maar het zegt wel iets over hoe breed geaccepteerd het lijkt te zijn.


Antwoord 11

Misschien kunt u overwegen om macro’s te gebruiken om deze functie te implementeren, de noodzaak om de functie of het veld opnieuw in de macro te gebruiken.

Other episodes