Erweitern einer Struktur in C

Lesezeit: 7 Minuten

Benutzeravatar von rubndsouza
rubndsouza

Ich bin kürzlich auf den Code eines Kollegen gestoßen, der so aussah:

typedef struct A {
  int x;
}A;

typedef struct B {
  A a;
  int d;
}B;

void fn(){
  B *b;
  ((A*)b)->x = 10;
}

Seine Erklärung war, dass da struct A war das erste Mitglied von struct BAlso b->x wäre dasselbe wie b->a.x und bietet eine bessere Lesbarkeit.
Das ist sinnvoll, aber wird dies als gute Praxis angesehen? Und funktioniert das plattformübergreifend? Derzeit läuft dies gut auf GCC.

Benutzeravatar von paxdiablo
paxdiablo

Ja, es funktioniert plattformübergreifend(a)aber das tut es nicht Notwendig mach es zu einer guten idee.

Gemäß dem ISO C-Standard (alle Zitate unten stammen aus C11), 6.7.2.1 Structure and union specifiers /15es darf keine Polsterung vorhanden sein Vor das erste Element einer Struktur

Zusätzlich, 6.2.7 Compatible type and composite type besagt, dass:

Zwei Typen haben einen kompatiblen Typ, wenn ihre Typen gleich sind

und es ist unbestritten, dass die A und A-within-B Typen sind identisch.

Das bedeutet, dass der Speicher auf die zugreift A Felder sind in beiden gleich A und B Typen, da wären die sinnvoller b->a.x das ist wahrscheinlich was Sie sollte verwenden, wenn Sie in Zukunft Bedenken hinsichtlich der Wartbarkeit haben.

Und obwohl Sie sich normalerweise um striktes Typ-Aliasing kümmern müssten, glaube ich nicht, dass dies hier zutrifft. Es ist illegal zu Alias-Zeigern, aber der Standard hat spezifische Ausnahmen.

6.5 Expressions /7 nennt einige dieser Ausnahmen mit der Fußnote:

Die Absicht dieser Liste ist es, die Umstände anzugeben, unter denen ein Objekt Alias ​​sein kann oder nicht.

Die aufgeführten Ausnahmen sind:

  • a type compatible with the effective type of the object;
  • einige andere Ausnahmen, die uns hier nicht zu interessieren brauchen; und
  • 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).

Das, kombiniert mit den oben erwähnten Struct-Padding-Regeln, einschließlich des Satzes:

Ein Zeiger auf ein Strukturobjekt, geeignet umgewandelt, zeigt auf sein Anfangselement

scheint darauf hinzudeuten, dass dieses Beispiel ausdrücklich zugelassen ist. Der Kernpunkt, an den wir uns hier erinnern müssen, ist die Art des Ausdrucks ((A*)b) ist A*nicht B*. Das macht die Variablen für die Zwecke des uneingeschränkten Aliasings kompatibel.

Das ist meine Lektüre der relevanten Teile des Standards, ich habe mich schon einmal geirrt (b)aber das bezweifle ich in diesem Fall.

Wenn Sie also eine haben echt Dafür muss es funktionieren, aber ich würde alle Einschränkungen im Code dokumentieren sehr in der Nähe der Strukturen, um in Zukunft nicht gebissen zu werden.


(a) Im allgemeinen Sinne. Natürlich der Codeschnipsel:

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

wird undefiniertes Verhalten sein, weil b ist nicht auf etwas Sinnvolles initialisiert. Aber ich gehe davon aus, dass dies nur ein Beispielcode ist, der Ihre Frage veranschaulichen soll. Wenn sich jemand darüber Sorgen macht, stellen Sie es sich stattdessen wie folgt vor:

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

(b) Wie meine Frau Ihnen sagen wird, häufig und mit wenig Aufforderung 🙂

Benutzeravatar von unwind
entspannen

Ich werde mich auf die Beine stellen und @paxdiablo in diesem Fall widersprechen: Ich denke, es ist eine gute Idee, und es ist sehr verbreitet in großem Code in Produktionsqualität.

Es ist im Grunde der offensichtlichste und netteste Weg, vererbungsbasierte objektorientierte Datenstrukturen in C zu implementieren. Beginnen Sie mit der Deklaration von struct B mit einer Instanz von struct A bedeutet “B ist eine Unterklasse von A”. Die Tatsache, dass das erste Strukturmitglied garantiert 0 Bytes vom Anfang der Struktur ist, macht es sicher, und es ist meiner Meinung nach grenzwertig schön.

Es ist weit verbreitet und wird in Code basierend auf dem bereitgestellt GObject Bibliothek, wie das GTK+ User Interface Toolkit und die GNOME-Desktopumgebung.

Natürlich müssen Sie “wissen, was Sie tun”, aber das ist im Allgemeinen immer der Fall, wenn Sie komplizierte Typbeziehungen in C implementieren. 🙂

Im Fall von GObject und GTK+ gibt es jede Menge unterstützende Infrastruktur und Dokumentation, die dabei helfen: Es ist ziemlich schwer, das zu vergessen. Es könnte bedeuten, dass Sie das Erstellen einer neuen Klasse nicht so schnell erledigen wie in C++, aber das ist vielleicht zu erwarten, da es in C keine native Unterstützung für Klassen gibt.

  • Mehr als grenzwertig schön, eher an der Grenze zu unberechenbarem Verhalten. “Die Tatsache, dass das erste Strukturelement garantiert 0 Bytes vom Anfang der Struktur ist …” kann sich je nach Compilerimplementierung ändern. Ist es ein C++-Standard? Selbst wenn es Teil des Standards wäre, könnte es Leuten, die die Codebasis nicht kennen, Wartungsprobleme bereiten, die zuerst die Typen herausfinden müssten!

    – Atmaram Shetye

    27. Oktober 2017 um 8:56 Uhr


Das ist eine schreckliche Idee. Sobald jemand vorbeikommt und ein weiteres Feld vor Struktur B einfügt, explodiert Ihr Programm. Und was ist daran so falsch b.a.x?

Benutzeravatar von dureuill
dureuill

Alles, was die Typprüfung umgeht, sollte generell vermieden werden. Dieser Hack verlässt sich auf die Reihenfolge der Deklarationen und weder die Umwandlung noch diese Reihenfolge kann vom Compiler erzwungen werden.

Es sollte plattformübergreifend funktionieren, aber ich denke nicht, dass es eine gute Praxis ist.

Wenn Sie wirklich tief verschachtelte Strukturen haben (man muss sich allerdings fragen, warum), dann sollten Sie eine temporäre lokale Variable verwenden, um auf die Felder zuzugreifen:

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;

Oder wenn Sie nicht kopieren möchten a eine lange:

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

Das zeigt woher a kommt und es erfordert keinen Gips.

Java und C# stellen auch regelmäßig Konstrukte wie “cba” zur Verfügung, ich sehe nicht, was das Problem ist. Wenn Sie objektorientiertes Verhalten simulieren möchten, sollten Sie die Verwendung einer objektorientierten Sprache (wie C ++) in Betracht ziehen, da das “Erweitern von Strukturen” in der von Ihnen vorgeschlagenen Weise weder Kapselung noch Laufzeitpolymorphismus bietet (obwohl man argumentieren kann dass ((A*)b) einer “dynamischen Besetzung” ähnlich ist).

Es tut mir leid, allen anderen Antworten hier nicht zuzustimmen, aber dieses System entspricht nicht Standard C. Es ist nicht akzeptabel, zwei Zeiger mit unterschiedlichen Typen zu haben, die gleichzeitig auf denselben Ort zeigen. Dies wird als Aliasing bezeichnet und ist von den strengen Aliasing-Regeln in C99 und vielen anderen Standards nicht erlaubt. Eine weniger hässliche Vorgehensweise wäre die Verwendung von Inline-Getter-Funktionen, die dann nicht so ordentlich aussehen müssen. Oder ist das vielleicht die Aufgabe einer Gewerkschaft? Es ist ausdrücklich erlaubt, einen von mehreren Typen zu halten, es gibt jedoch auch eine Vielzahl anderer Nachteile.

Kurz gesagt, diese Art von unsauberem Casting zur Erzeugung von Polymorphismus wird von den meisten C-Standards nicht erlaubt, nur weil es auf Ihrem Compiler zu funktionieren scheint, bedeutet das nicht, dass es akzeptabel ist. Hier finden Sie eine Erklärung, warum dies nicht zulässig ist und warum Compiler auf hohen Optimierungsstufen Code beschädigen können, der diesen Regeln nicht folgt http://en.wikipedia.org/wiki/Aliasing_%28computing%29#Conflicts_with_optimization

  • Tatsächlich ist es in der beschriebenen Verwendung kein Aliasing und es ist ausdrücklich konform mit C99 6.7.2.1p13: „Ein Zeiger auf ein Strukturobjekt, geeignet konvertiert, zeigt auf sein Anfangselement, und umgekehrt.”

    – Greg A. Woods

    7. März 2016 um 2:30 Uhr

Benutzeravatar der Community
Gemeinschaft

Ja, es wird funktionieren. Und es ist eines der Kernprinzipien von Objektorientiert mit C. Weitere Beispiele zum Erweitern (dh Vererbung) finden Sie in dieser Antwort ‘Objektorientierung in C’.

  • Tatsächlich ist es in der beschriebenen Verwendung kein Aliasing und es ist ausdrücklich konform mit C99 6.7.2.1p13: „Ein Zeiger auf ein Strukturobjekt, geeignet konvertiert, zeigt auf sein Anfangselement, und umgekehrt.”

    – Greg A. Woods

    7. März 2016 um 2:30 Uhr

Benutzeravatar von Patrick Collins
Patrick Collins

Das ist vollkommen legal und meiner Meinung nach ziemlich elegant. Ein Beispiel dafür im Produktionscode finden Sie unter GObject-Dokumentation:

Dank dieser einfachen Bedingungen ist es möglich, den Typ jeder Objektinstanz zu erkennen, indem Sie Folgendes tun:

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

oder schneller:

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

Ich persönlich denke, dass Gewerkschaften hässlich sind und dazu neigen, zu riesig zu führen switch Anweisungen, was ein großer Teil dessen ist, was Sie durch das Schreiben von OO-Code vermieden haben. Ich schreibe selbst eine beträchtliche Menge an Code in diesem Stil – typischerweise das erste Mitglied der struct enthält Funktionszeiger, die für den betreffenden Typ wie eine vtable funktionieren können.

1413700cookie-checkErweitern einer Struktur in C

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy