Zugriff auf inaktives Gewerkschaftsmitglied und undefiniertes Verhalten?

128

Ich hatte den Eindruck, dass der Zugriff auf ein unionanderes Mitglied als den letzten Satz UB ist, aber ich kann anscheinend keine solide Referenz finden (außer Antworten, die behaupten, es sei UB, aber ohne Unterstützung durch den Standard).

Ist es also undefiniertes Verhalten?

Luchian Grigore
quelle
3
C99 (und ich glaube auch C ++ 11) erlaubt ausdrücklich das Typ-Punning mit Gewerkschaften. Ich denke also, dass es unter "implementierungsdefiniertes" Verhalten fällt.
Mysticial
1
Ich habe es mehrmals benutzt, um von individuellem int zu char zu konvertieren. Ich weiß also definitiv, dass es nicht undefiniert ist. Ich habe es auf dem Sun CC-Compiler verwendet. Es kann also immer noch vom Compiler abhängig sein.
go4sri
42
@ go4sri: Klar, du weißt nicht, was es bedeutet, wenn Verhalten undefiniert ist. Die Tatsache, dass es in einigen Fällen für Sie zu funktionieren schien, widerspricht nicht seiner Undefiniertheit.
Benjamin Lindley
4
@Mysticial, der Blog-Beitrag, auf den Sie verlinken, bezieht sich ganz speziell auf C99. Diese Frage ist nur für C ++ markiert.
Davmac

Antworten:

131

Die Verwirrung ist, dass C explizit Typ-Punning durch eine Union erlaubt, während C ++ () hat keine solche Erlaubnis.

6.5.2.3 Struktur und Gewerkschaftsmitglieder

95) Wenn das Element, das zum Lesen des Inhalts eines Vereinigungsobjekts verwendet wird, nicht mit dem Element identisch ist, das zuletzt zum Speichern eines Werts im Objekt verwendet wurde, wird der entsprechende Teil der Objektdarstellung des Werts als Objektdarstellung im neuen Objekt neu interpretiert Typ wie in 6.2.6 beschrieben (ein Prozess, der manchmal als "Typ Punning" bezeichnet wird). Dies könnte eine Trap-Darstellung sein.

Die Situation mit C ++:

9.5 Gewerkschaften [class.union]

In einer Union kann höchstens eines der nicht statischen Datenelemente jederzeit aktiv sein, dh der Wert von höchstens einem der nicht statischen Datenelemente kann jederzeit in einer Union gespeichert werden.

C ++ hat später eine Sprache, die die Verwendung von Gewerkschaften erlaubt, die structs mit gemeinsamen Anfangssequenzen enthalten. Dies erlaubt jedoch keine Typ-Punning.

Um festzustellen, ob Union Type-Punning ist in C ++ , müssen wir weiter suchen. Erinnere dich daran ist eine normative Referenz für C ++ 11 (und C99 hat eine ähnliche Sprache wie C11, die das Punning von Unionstypen ermöglicht):

3.9 Typen [basic.types]

4 - Die Objektdarstellung eines Objekts vom Typ T ist die Folge von N vorzeichenlosen Zeichenobjekten, die vom Objekt vom Typ T aufgenommen werden, wobei N gleich sizeof (T) ist. Die Wertdarstellung eines Objekts ist die Menge von Bits, die den Wert vom Typ T enthalten. Bei trivial kopierbaren Typen ist die Wertdarstellung eine Menge von Bits in der Objektdarstellung, die einen Wert bestimmt, der ein diskretes Element einer Implementierung ist. definierter Wertesatz. 42
42) Es ist beabsichtigt, dass das Speichermodell von C ++ mit dem von ISO / IEC 9899 Programmiersprache C kompatibel ist.

Besonders interessant wird es, wenn wir lesen

3.8 Objektlebensdauer [basic.life]

Die Lebensdauer eines Objekts vom Typ T beginnt, wenn: - eine Speicherung mit der richtigen Ausrichtung und Größe für Typ T erhalten wird und - wenn das Objekt eine nicht triviale Initialisierung aufweist, ist seine Initialisierung abgeschlossen.

Für einen primitiven Typ (der ipso facto eine triviale Initialisierung aufweist), der in einer Vereinigung enthalten ist, umfasst die Lebensdauer des Objekts mindestens die Lebensdauer der Vereinigung selbst. Dies ermöglicht es uns, aufzurufen

3.9.2 Verbindungstypen [basic.compound]

Befindet sich ein Objekt vom Typ T an einer Adresse A, so soll ein Zeiger vom Typ cv T *, dessen Wert die Adresse A ist, auf dieses Objekt zeigen, unabhängig davon, wie der Wert erhalten wurde.

Unter der Annahme, dass es sich bei der Operation, an der wir interessiert sind, um Typ-Punning handelt, dh den Wert eines nicht aktiven Gewerkschaftsmitglieds annimmt, und vorausgesetzt, dass wir einen gültigen Verweis auf das Objekt haben, auf das sich dieses Mitglied bezieht, ist diese Operation von Wert -Wertumwandlung:

4.1 L-Wert-zu-Wert-Konvertierung [conv.lval]

Ein Gl-Wert eines Nicht-Funktions- oder Nicht-Array-Typs Tkann in einen Wert umgewandelt werden. Wenn Tes sich um einen unvollständigen Typ handelt, ist ein Programm, das diese Konvertierung erfordert, fehlerhaft. Wenn das Objekt, auf das sich der Wert bezieht, kein Objekt vom Typ Tund kein Objekt eines Typs ist, von dem abgeleitet wurde T, oder wenn das Objekt nicht initialisiert ist, hat ein Programm, das diese Konvertierung erfordert, ein nicht definiertes Verhalten.

Die Frage ist dann, ob ein Objekt, das ein nicht aktives Gewerkschaftsmitglied ist, durch Speichern im aktiven Gewerkschaftsmitglied initialisiert wird. Soweit ich das beurteilen kann, ist dies nicht der Fall und obwohl:

  • Eine Union wird in den charArray-Speicher und zurück kopiert (3.9: 2), oder
  • Eine Vereinigung wird byteweise in eine andere Vereinigung des gleichen Typs kopiert (3.9: 3), oder
  • Auf eine Union wird dann über Sprachgrenzen hinweg von einem Programmelement zugegriffen, das ISO / IEC 9899 (soweit definiert) entspricht (3.9: 4, Anmerkung 42)

Der Zugriff eines nicht aktiven Mitglieds auf eine Union ist definiert und folgt der Objekt- und Wertdarstellung. Der Zugriff ohne eine der oben genannten Interpositionen ist ein undefiniertes Verhalten. Dies hat Auswirkungen auf die Optimierungen, die für ein solches Programm durchgeführt werden dürfen, da bei der Implementierung natürlich davon ausgegangen werden kann, dass kein undefiniertes Verhalten auftritt.

Das heißt, obwohl wir einem nicht aktiven Gewerkschaftsmitglied einen legitimen Wert bilden können (weshalb die Zuordnung zu einem nicht aktiven Mitglied ohne Konstruktion in Ordnung ist), wird dies als nicht initialisiert angesehen.

ecatmur
quelle
5
3.8 / 1 besagt, dass die Lebensdauer eines Objekts endet, wenn sein Speicher wiederverwendet wird. Das bedeutet für mich, dass ein nicht aktives Mitglied zu Lebzeiten einer Gewerkschaft beendet wurde, weil sein Speicher für das aktive Mitglied wiederverwendet wurde. Das würde bedeuten, dass Sie in der Verwendung des Mitglieds eingeschränkt sind (3.8 / 6).
Bames53
2
Bei dieser Interpretation enthält dann jedes Speicherbit gleichzeitig Objekte aller Typen, die trivial initiierbar sind und eine angemessene Ausrichtung aufweisen. Die Lebensdauer eines nicht trivial initiierbaren Typs endet also sofort, da sein Speicher für alle diese anderen Typen wiederverwendet wird ( und nicht neu starten, weil sie nicht trivial initiierbar sind)?
Bames53
3
Der Wortlaut 4.1 ist vollständig und vollständig gebrochen und wurde seitdem neu geschrieben. Es hat alle Arten von vollkommen gültigen Dingen nicht zugelassen: Es hat benutzerdefinierte memcpyImplementierungen (Zugriff auf Objekte mit unsigned charl-Werten) nicht zugelassen, Zugriffe auf *pAfter nicht zugelassen int *p = 0; const int *const *pp = &p;(obwohl die implizite Konvertierung von int**to const int*const*gültig ist) und sogar den Zugriff cnachher nicht zugelassen struct S s; const S &c = s;. CWG Ausgabe 616 . Erlaubt der neue Wortlaut dies? Es gibt auch [basic.lval].
2
@ Omnifarious: Das wäre sinnvoll, müsste aber auch klarstellen (und der C-Standard muss übrigens auch klarstellen), was der unäre &Operator bedeutet, wenn er auf ein Gewerkschaftsmitglied angewendet wird. Ich würde denken, dass der resultierende Zeiger verwendet werden sollte, um zumindest bis zur nächsten direkten oder indirekten Verwendung eines anderen Mitgliedswerts auf das Mitglied zuzugreifen, aber in gcc ist der Zeiger nicht einmal so lange verwendbar, was eine Frage aufwirft, was der &Betreiber soll bedeuten.
Supercat
4
Eine Frage zu "Erinnern Sie sich daran, dass c99 eine normative Referenz für C ++ 11 ist" Ist das nicht nur relevant, wenn der c ++ - Standard explizit auf den C-Standard verweist (z. B. für die Funktionen der c-Bibliothek)?
MikeMB
28

Der C ++ 11-Standard sagt es so

9.5 Gewerkschaften

In einer Union kann höchstens eines der nicht statischen Datenelemente jederzeit aktiv sein, dh der Wert von höchstens einem der nicht statischen Datenelemente kann jederzeit in einer Union gespeichert werden.

Wenn nur ein Wert gespeichert ist, wie können Sie einen anderen lesen? Es ist einfach nicht da.


Die gcc-Dokumentation listet dies unter Implementierungsdefiniertes Verhalten auf

  • Auf ein Mitglied eines Vereinigungsobjekts wird mit einem Mitglied eines anderen Typs zugegriffen (C90 6.3.2.3).

Die relevanten Bytes der Darstellung des Objekts werden als Objekt des für den Zugriff verwendeten Typs behandelt. Siehe Typ-Punning. Dies kann eine Trap-Darstellung sein.

Dies zeigt an, dass dies vom C-Standard nicht verlangt wird.


05.01.2016: Durch die Kommentare wurde ich mit dem C99-Fehlerbericht Nr. 283 verknüpft , der dem C-Standarddokument einen ähnlichen Text als Fußnote hinzufügt:

78a) Wenn das Element, das für den Zugriff auf den Inhalt eines Vereinigungsobjekts verwendet wird, nicht mit dem Element identisch ist, das zuletzt zum Speichern eines Werts im Objekt verwendet wurde, wird der entsprechende Teil der Objektdarstellung des Werts als Objektdarstellung im neuen Objekt neu interpretiert Typ wie in 6.2.6 beschrieben (ein Prozess, der manchmal als "Typ Punning" bezeichnet wird). Dies könnte eine Trap-Darstellung sein.

Ich bin mir nicht sicher, ob es viel klarstellt, wenn man bedenkt, dass eine Fußnote für den Standard nicht normativ ist.

Bo Persson
quelle
10
@LuchianGrigore: UB ist nicht das, was der Standard sagt, sondern das, was der Standard nicht beschreibt, wie es funktionieren soll. Dies ist genau der Fall. Beschreibt der Standard, was passiert? Sagt es, dass die Implementierung definiert ist? Nein und nein. Also ist es UB. In Bezug auf das Argument "Mitglieder haben dieselbe Speicheradresse" müssen Sie außerdem auf die Aliasing-Regeln verweisen, die Sie erneut zu UB bringen.
Yakov Galka
5
@Luchian: Es ist ziemlich klar, was aktiv bedeutet, "das heißt, der Wert von höchstens einem der nicht statischen Datenelemente kann jederzeit in einer Union gespeichert werden."
Benjamin Lindley
5
@LuchianGrigore: Ja, das gibt es. Es gibt unendlich viele Fälle, die der Standard nicht behandelt (und nicht behandeln kann). (C ++ ist eine vollständige Turing-VM, daher unvollständig.) Na und? Es erklärt, was "aktiv" bedeutet, siehe obiges Zitat nach "das ist".
Yakov Galka
8
@LuchianGrigore: Das Auslassen einer expliziten Definition des Verhaltens ist laut Definitionsabschnitt auch ein unberücksichtigtes undefiniertes Verhalten.
jxh
5
@Claudiu Das ist UB aus einem anderen Grund - es verstößt gegen striktes Aliasing.
Mysticial
18

Ich denke, der Standard kommt dem undefinierten Verhalten am nächsten, wenn er das Verhalten für eine Union definiert, die eine gemeinsame Anfangssequenz enthält (C99, §6.5.2.3 / 5):

Eine besondere Garantie wird gegeben, um die Verwendung von Gewerkschaften zu vereinfachen: Wenn eine Gewerkschaft mehrere Strukturen enthält, die eine gemeinsame Anfangssequenz haben (siehe unten), und wenn das Gewerkschaftsobjekt derzeit eine dieser Strukturen enthält, ist es zulässig, die gemeinsame zu inspizieren erster Teil von jedem von ihnen überall dort, wo eine Erklärung des vollständigen Typs der Gewerkschaft sichtbar ist. Zwei Strukturen teilen eine gemeinsame Anfangssequenz, wenn entsprechende Elemente kompatible Typen (und für Bitfelder die gleichen Breiten) für eine Sequenz von einem oder mehreren Anfangselementen haben.

C ++ 11 bietet ähnliche Anforderungen / Berechtigungen in §9.2 / 19:

Wenn eine Standard-Layout-Vereinigung zwei oder mehr Standard-Layout-Strukturen enthält, die eine gemeinsame Anfangssequenz haben, und wenn das Standard-Layout-Vereinigungsobjekt derzeit eine dieser Standard-Layout-Strukturen enthält, kann der gemeinsame Anfangsteil einer beliebigen Struktur überprüft werden von ihnen. Zwei Standardlayoutstrukturen haben eine gemeinsame Anfangssequenz, wenn entsprechende Elemente layoutkompatible Typen haben und entweder kein Element ein Bitfeld ist oder beide Bitfelder mit derselben Breite für eine Sequenz von einem oder mehreren Anfangselementen sind.

Obwohl es keiner direkt angibt, haben beide eine starke Implikation, dass das "Inspizieren" (Lesen) eines Mitglieds nur dann "erlaubt" ist , wenn 1) es (ein Teil) des zuletzt geschriebenen Mitglieds ist oder 2) Teil einer gemeinsamen Initiale ist Reihenfolge.

Das ist keine direkte Aussage, dass es undefiniertes Verhalten ist, etwas anderes zu tun, aber es ist das nächste, von dem ich weiß.

Jerry Sarg
quelle
Um dies zu vervollständigen, müssen Sie wissen, was "Layout-kompatible Typen" für C ++ oder "kompatible Typen" für C sind.
Michael Anderson
2
@ MichaelAnderson: Ja und nein. Sie müssen sich mit diesen befassen, wenn / wenn Sie sicher sein möchten, ob etwas unter diese Ausnahme fällt - aber die eigentliche Frage hier ist, ob etwas, das eindeutig außerhalb der Ausnahme liegt, UB wirklich gibt. Ich denke, das ist hier stark genug impliziert, um die Absicht klar zu machen, aber ich denke nicht, dass es jemals direkt gesagt wird.
Jerry Coffin
Diese "gemeinsame Anfangssequenz" hat möglicherweise nur 2 oder 3 meiner Projekte aus dem Rewrite Bin gespeichert. Ich war wütend, als ich zum ersten Mal las, dass die meisten Anwendungen von unions undefiniert sind, da ich von einem bestimmten Blog den Eindruck bekommen hatte, dass dies in Ordnung ist, und mehrere große Strukturen und Projekte darum herum gebaut habe. Jetzt denke ich, dass ich vielleicht doch in Ordnung bin, da meine unions Klassen mit den gleichen Typen an der Vorderseite enthalten
underscore_d
@ JerryCoffin, ich denke, Sie haben die gleiche Frage wie ich angedeutet: Was wäre, wenn unsere zB a und a unionenthält ? Ich würde annehmen, dass dieser Vorbehalt auch hier gilt, aber es ist sehr bewusst formuliert, nur s zuzulassen . Zum Glück verwende ich diese bereits anstelle von rohen Grundelementen: Ouint8_tclass Something { uint8_t myByte; [...] };struct
underscore_d
@underscore_d: Der C-Standard deckt zumindest diese Frage ab: "Ein Zeiger auf ein Strukturobjekt, das entsprechend konvertiert wurde, zeigt auf sein ursprüngliches Element (oder, wenn dieses Element ein Bitfeld ist, auf die Einheit, in der es sich befindet). , und umgekehrt."
Jerry Coffin
12

Was in den verfügbaren Antworten noch nicht erwähnt wird, ist die Fußnote 37 in Abschnitt 21 Absatz 21.2.5:

Beachten Sie, dass der Aggregattyp keinen Vereinigungstyp enthält, da ein Objekt mit Vereinigungstyp jeweils nur ein Mitglied enthalten kann.

Diese Anforderung scheint eindeutig zu implizieren, dass Sie nicht in ein Mitglied schreiben und in einem anderen lesen dürfen. In diesem Fall kann es sich um ein undefiniertes Verhalten aufgrund fehlender Spezifikation handeln.

mpu
quelle
Viele Implementierungen dokumentieren ihre Speicherformate und Layoutregeln. Eine solche Spezifikation würde in vielen Fällen implizieren, wie sich das Lesen des Speichers eines Typs und das Schreiben eines anderen Typs auswirken würde, wenn keine Regeln vorliegen, nach denen Compiler ihr definiertes Speicherformat nur dann verwenden müssen, wenn Dinge mithilfe von Zeigern gelesen und geschrieben werden eines Zeichentyps.
Supercat
-3

Ich erkläre das gut mit einem Beispiel.
Nehmen wir an, wir haben die folgende Vereinigung:

union A{
   int x;
   short y[2];
};

Ich gehe davon aus, dass dies sizeof(int)4 ergibt und dass dies 2 sizeof(short)ergibt.
Wenn Sie so gut schreiben union A a = {10}, erstellen Sie eine neue Variable vom Typ A, in die der Wert 10 eingegeben wird.

Ihr Gedächtnis sollte folgendermaßen aussehen: (Denken Sie daran, dass alle Gewerkschaftsmitglieder denselben Standort erhalten.)

       | x |
       | y [0] | y [1] |
       -----------------------------------------
   a-> | 0000 0000 | 0000 0000 | 0000 0000 | 0000 1010 |
       -----------------------------------------

Wie Sie sehen konnten, ist der Wert von ax 10, der Wert von ay 1 ist 10 und der Wert von ay [0] ist 0.

Was passiert nun, wenn ich das mache?

a.y[0] = 37;

Unsere Erinnerung wird so aussehen:

       | x |
       | y [0] | y [1] |
       -----------------------------------------
   a-> | 0000 0000 | 0010 0101 | 0000 0000 | 0000 1010 |
       -----------------------------------------

Dadurch wird der Wert von ax auf 2424842 (in Dezimalzahl) geändert.

Wenn Ihre Gewerkschaft einen Float oder Double hat, ist Ihre Speicherzuordnung aufgrund der Art und Weise, wie Sie genaue Zahlen speichern, eher ein Chaos. Weitere Informationen finden Sie hier .

elyashiv
quelle
18
:) Das habe ich nicht gefragt. Ich weiß, was intern passiert. Ich weiß, dass es funktioniert. Ich fragte, ob es im Standard ist.
Luchian Grigore