Ich habe Gewerkschaften früher bequem benutzt; Heute war ich alarmiert, als ich diesen Beitrag las und erfuhr, dass dieser Code
union ARGB
{
uint32_t colour;
struct componentsTag
{
uint8_t b;
uint8_t g;
uint8_t r;
uint8_t a;
} components;
} pixel;
pixel.colour = 0xff040201; // ARGB::colour is the active member from now on
// somewhere down the line, without any edit to pixel
if(pixel.components.a) // accessing the non-active member ARGB::components
ist tatsächlich undefiniertes Verhalten, dh das Lesen von einem anderen Gewerkschaftsmitglied als dem, an das kürzlich geschrieben wurde, führt zu undefiniertem Verhalten. Wenn dies nicht die beabsichtigte Verwendung von Gewerkschaften ist, was ist das? Kann jemand es bitte ausführlich erklären?
Aktualisieren:
Ich wollte im Nachhinein ein paar Dinge klären.
- Die Antwort auf die Frage ist für C und C ++ nicht dieselbe. Mein ignorantes jüngeres Ich hat es sowohl als C als auch als C ++ markiert.
- Nachdem ich den Standard von C ++ 11 durchgesehen hatte, konnte ich nicht abschließend sagen, dass der Zugriff auf / die Inspektion eines nicht aktiven Gewerkschaftsmitglieds undefiniert / nicht spezifiziert / implementierungsdefiniert ist. Alles was ich finden konnte war §9.5 / 1:
Wenn eine Standard-Layout-Union mehrere Standard-Layout-Strukturen enthält, die eine gemeinsame Anfangssequenz haben, und wenn ein Objekt dieses Standard-Layout-Union-Typs eine der Standard-Layout-Strukturen enthält, kann die gemeinsame Anfangssequenz einer beliebigen Struktur überprüft werden von Standard-Layout-Strukturelementen. §9.2 / 19: Zwei Standardlayoutstrukturen teilen 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 Anfangsbuchstaben sind Mitglieder.
- In C (ab C99 TC3 - DR 283 ) ist dies legal ( danke an Pascal Cuoq , der dies angesprochen hat). Der Versuch, dies zu tun , kann jedoch immer noch zu undefiniertem Verhalten führen , wenn der gelesene Wert für den Typ, den er durchliest, ungültig ist (sogenannte "Trap-Darstellung"). Andernfalls wird der gelesene Wert implementierungsdefiniert.
C89 / 90 hat dies unter nicht spezifiziertem Verhalten (Anhang J) herausgestellt, und das Buch von K & R besagt, dass die Implementierung definiert ist. Zitat von K & R:
Dies ist der Zweck einer Union - eine einzelne Variable, die einen von mehreren Typen rechtmäßig enthalten kann. [...] solange die Verwendung konsistent ist: Der abgerufene Typ muss der zuletzt gespeicherte Typ sein. Es liegt in der Verantwortung des Programmierers, zu verfolgen, welcher Typ derzeit in einer Union gespeichert ist. Die Ergebnisse sind implementierungsabhängig, wenn etwas als ein Typ gespeichert und als ein anderer extrahiert wird.
Auszug aus Stroustrups TC ++ PL (Schwerpunkt Mine)
Die Verwendung von Gewerkschaften kann für die Kompatibilität von Daten von wesentlicher Bedeutung sein, [...] die manchmal für die "Typkonvertierung " missbraucht werden .
Vor allem diese Frage (deren Titel seit meiner Anfrage unverändert geblieben ist) wurde mit der Absicht gestellt, den Zweck von Gewerkschaften zu verstehen UND nicht darüber, was der Standard erlaubt. ZB Die Verwendung der Vererbung für die Wiederverwendung von Code ist natürlich nach dem C ++ - Standard zulässig, aber Es war nicht der Zweck oder die ursprüngliche Absicht, Vererbung als C ++ - Sprachfunktion einzuführen . Dies ist der Grund, warum Andreys Antwort weiterhin die akzeptierte bleibt.
quelle
b, g, r,
unda
kann nicht zusammenhängend sein und somit nicht mit dem Layout von a übereinstimmenuint32_t
. Dies ist zusätzlich zu den Endianess-Problemen, auf die andere hingewiesen haben.scouring C++11's standard I couldn't conclusively say that it calls out accessing/inspecting a non-active union member is undefined [...] All I could find was §9.5/1
...Ja wirklich? Sie eine Ausnahme zitieren Note , nicht die wichtigsten Punkt gleich zu Beginn des Absatzes : „In einer Union, höchstens einer der nicht-statischen Datenelemente können jederzeit aktiv sein, dass der Wert von höchstens eines von Die nicht statischen Datenelemente können jederzeit in einer Union gespeichert werden. " - und bis zu p4: "Im Allgemeinen muss man explizite Destruktoraufrufe verwenden und neue Operatoren platzieren, um das aktive Mitglied einer Gewerkschaft zu ändern "Antworten:
Der Zweck der Gewerkschaften liegt auf der Hand, aber aus irgendeinem Grund vermissen die Leute ihn ziemlich oft.
Der Zweck der Vereinigung besteht darin, Speicher zu sparen, indem derselbe Speicherbereich zum Speichern verschiedener Objekte zu unterschiedlichen Zeiten verwendet wird. Das ist es.
Es ist wie ein Zimmer in einem Hotel. Verschiedene Menschen leben für nicht überlappende Zeiträume darin. Diese Leute treffen sich nie und wissen im Allgemeinen nichts voneinander. Durch die ordnungsgemäße Verwaltung der zeitlichen Aufteilung der Zimmer (dh durch Sicherstellen, dass nicht gleichzeitig verschiedene Personen einem Zimmer zugewiesen werden) kann ein relativ kleines Hotel einer relativ großen Anzahl von Personen Unterkünfte bieten, was auch Hotels sind sind für.
Genau das macht Union. Wenn Sie wissen, dass mehrere Objekte in Ihrem Programm Werte mit nicht überlappenden Wertlebensdauern enthalten, können Sie diese Objekte zu einer Vereinigung "zusammenführen" und so Speicher sparen. So wie ein Hotelzimmer zu jedem Zeitpunkt höchstens einen "aktiven" Mieter hat, hat eine Gewerkschaft zu jedem Zeitpunkt der Programmzeit höchstens ein "aktives" Mitglied. Nur das "aktive" Mitglied kann gelesen werden. Indem Sie in ein anderes Mitglied schreiben, wechseln Sie den Status "aktiv" zu diesem anderen Mitglied.
Aus irgendeinem Grund wurde dieser ursprüngliche Zweck der Gewerkschaft durch etwas völlig anderes "außer Kraft gesetzt": ein Mitglied einer Gewerkschaft zu schreiben und es dann durch ein anderes Mitglied zu inspizieren. Diese Art der Neuinterpretation des Gedächtnisses (auch bekannt als "Typ Punning") ist
keine gültige Verwendung von Gewerkschaften. Es führt im Allgemeinen dazu, dass undefiniertesVerhalten in C89 / 90 als implementierungsdefiniertesVerhaltenbeschrieben wird.BEARBEITEN: Die Verwendung von Gewerkschaften zum Zwecke der Typisierung (dh Schreiben eines Mitglieds und anschließendes Lesen eines anderen) wurde in einer der technischen Berichtigungen zum C99-Standard ausführlicher definiert (siehe DR # 257 und DR # 283 ). Beachten Sie jedoch, dass dies Sie formal nicht vor undefiniertem Verhalten schützt, indem Sie versuchen, eine Trap-Darstellung zu lesen.
quelle
<time.h>
Windows und Unix gesehen. Es reicht nicht aus, es als "ungültig" und "undefiniert" abzulehnen, wenn ich aufgefordert werde, Code zu verstehen, der genau so funktioniert.Sie können Gewerkschaften verwenden, um Strukturen wie die folgenden zu erstellen, die ein Feld enthalten, das angibt, welche Komponente der Vereinigung tatsächlich verwendet wird:
quelle
int
oderchar*
für 10 Objekte []; In welchem Fall kann ich anstelle von VAROBJECT für jeden Datentyp separate Strukturen deklarieren? Würde es nicht die Unordnung reduzieren und weniger Platz beanspruchen?Das Verhalten ist aus sprachlicher Sicht undefiniert. Bedenken Sie, dass unterschiedliche Plattformen unterschiedliche Einschränkungen hinsichtlich Speicherausrichtung und Endianness aufweisen können. Der Code in einer Big-Endian-Maschine im Vergleich zu einer Little-Endian-Maschine aktualisiert die Werte in der Struktur unterschiedlich. Um das Verhalten in der Sprache zu korrigieren, müssten alle Implementierungen dieselbe Endianness (und Speicherausrichtungsbeschränkungen ...) verwenden, was die Verwendung einschränkt.
Wenn Sie C ++ verwenden (Sie verwenden zwei Tags) und die Portabilität wirklich wichtig ist, können Sie einfach die Struktur verwenden und einen Setter bereitstellen, der
uint32_t
die Felder durch Bitmaskenoperationen entsprechend festlegt. Das gleiche kann in C mit einer Funktion gemacht werden.Bearbeiten : Ich hatte erwartet, dass AProgrammer eine Antwort aufschreibt, um abzustimmen und diese zu schließen. Wie einige Kommentare hervorgehoben haben, wird Endianness in anderen Teilen des Standards behandelt, indem jede Implementierung entscheiden lässt, was zu tun ist, und Ausrichtung und Polsterung können auch unterschiedlich gehandhabt werden. Nun sind die strengen Aliasing-Regeln, auf die sich AProgrammer implizit bezieht, hier ein wichtiger Punkt. Der Compiler darf Annahmen über die Änderung (oder das Fehlen einer Änderung) von Variablen treffen. Im Fall der Vereinigung könnte der Compiler Anweisungen neu anordnen und den Lesevorgang jeder Farbkomponente über den Schreibvorgang in die Farbvariable verschieben.
quelle
Die häufigste Verwendung, auf die
union
ich regelmäßig stoße, ist Aliasing .Folgendes berücksichtigen:
Was macht das? Es ermöglicht einen sauberen und übersichtlichen Zugriff auf
Vector3f vec;
die Mitglieder eines der beiden Namen:oder durch ganzzahligen Zugriff auf das Array
In einigen Fällen ist der Zugriff über den Namen das klarste, was Sie tun können. In anderen Fällen, insbesondere wenn die Achse programmgesteuert ausgewählt wird, ist es einfacher, auf die Achse über den numerischen Index zuzugreifen - 0 für x, 1 für y und 2 für z.
quelle
type-punning
was auch in der Frage erwähnt wird. Auch das Beispiel in der Frage zeigt ein ähnliches Beispiel.Wie Sie sagen, ist dies ein streng undefiniertes Verhalten, obwohl es auf vielen Plattformen "funktioniert". Der eigentliche Grund für die Verwendung von Gewerkschaften besteht darin, Variantendatensätze zu erstellen.
Natürlich brauchen Sie auch eine Art Diskriminator, um zu sagen, was die Variante tatsächlich enthält. Beachten Sie, dass Gewerkschaften in C ++ nicht viel Verwendung finden, da sie nur POD-Typen enthalten können - effektiv solche ohne Konstruktoren und Destruktoren.
quelle
In C war es eine gute Möglichkeit, so etwas wie eine Variante zu implementieren.
In Zeiten mit wenig Speicher benötigt diese Struktur weniger Speicher als eine Struktur mit allen Mitgliedern.
Übrigens bietet C.
um auf Bitwerte zuzugreifen.
quelle
Obwohl dies ein streng undefiniertes Verhalten ist, funktioniert es in der Praxis mit so ziemlich jedem Compiler. Es ist ein so weit verbreitetes Paradigma, dass jeder Compiler mit Selbstachtung in solchen Fällen "das Richtige" tun muss. Es ist sicherlich dem Typ-Punning vorzuziehen, das bei einigen Compilern möglicherweise fehlerhaften Code erzeugt.
quelle
In C ++ implementiert Boost Variant eine sichere Version der Union, um undefiniertes Verhalten so weit wie möglich zu verhindern.
Seine Leistung ist identisch mit dem
enum + union
Konstrukt (Stapel auch zugewiesen usw.), aber es verwendet eine Vorlagenliste von Typen anstelle derenum
:)quelle
Das Verhalten mag undefiniert sein, aber das bedeutet nur, dass es keinen "Standard" gibt. Alle anständigen Compiler bieten #pragmas an, um das Packen und Ausrichten zu steuern, können jedoch unterschiedliche Standardeinstellungen haben. Die Standardeinstellungen ändern sich auch in Abhängigkeit von den verwendeten Optimierungseinstellungen.
Gewerkschaften sind nicht nur platzsparend. Sie können modernen Compilern beim Typ Punning helfen. Wenn Sie
reinterpret_cast<>
alles haben, kann der Compiler keine Annahmen darüber treffen, was Sie tun. Möglicherweise muss es das, was es über Ihren Typ weiß, wegwerfen und erneut starten (ein Zurückschreiben in den Speicher erzwingen, was heutzutage im Vergleich zur CPU-Taktrate sehr ineffizient ist).quelle
Technisch gesehen ist es undefiniert, aber in Wirklichkeit behandeln die meisten (alle?) Compiler es genauso wie die Verwendung eines
reinterpret_cast
von einem Typ zum anderen, dessen Ergebnis die Implementierung definiert ist. Ich würde nicht den Schlaf über Ihren aktuellen Code verlieren.quelle
Für ein weiteres Beispiel für die tatsächliche Verwendung von Gewerkschaften serialisiert das CORBA-Framework Objekte mithilfe des Ansatzes der markierten Vereinigung. Alle benutzerdefinierten Klassen sind Mitglieder einer (großen) Union, und eine Ganzzahlkennung teilt dem Demarshaller mit, wie die Union zu interpretieren ist.
quelle
Andere haben die Architekturunterschiede erwähnt (Little - Big Endian).
Ich habe das Problem gelesen, dass sich die anderen ändern, da der Speicher für die Variablen gemeinsam genutzt wird. Wenn Sie in eine schreiben, ändern sich die anderen und je nach Typ kann der Wert bedeutungslos sein.
z.B. Vereinigung {float f; int i; } x;
Das Schreiben in xi wäre bedeutungslos, wenn Sie dann aus xf lesen - es sei denn, Sie haben dies beabsichtigt, um die Vorzeichen-, Exponenten- oder Mantissenkomponenten des Floats zu betrachten.
Ich denke, es gibt auch ein Problem mit der Ausrichtung: Wenn einige Variablen wortausgerichtet sein müssen, erhalten Sie möglicherweise nicht das erwartete Ergebnis.
z.B. Vereinigung {char c [4]; int i; } x;
Wenn hypothetisch auf einem Computer ein Zeichen wortausgerichtet werden müsste, würden c [0] und c [1] den Speicher mit i teilen, nicht jedoch c [2] und c [3].
quelle
memcpy()
von einem zum anderen verdoppeln . Einige Systeme könnenchar[]
Zuweisungen , die außerhalb von Strukturen / Gewerkschaften aus diesem und anderen Gründen auftreten, spekulativ ausrichten . Im vorliegenden Beispiel ist die Annahme, dassi
sich alle Elemente von überlappen,c[]
nicht portierbar, aber das liegt daran, dass es keine Garantie dafür gibtsizeof(int)==4
.In der C-Sprache, wie sie 1974 dokumentiert wurde, hatten alle Strukturmitglieder einen gemeinsamen Namespace, und die Bedeutung von "ptr-> member" wurde definiert als Hinzufügen der Verschiebung des Mitglieds zu "ptr" und Zugreifen auf die resultierende Adresse unter Verwendung des Mitgliedstyps. Dieses Design ermöglichte die Verwendung desselben ptr mit Mitgliedsnamen aus verschiedenen Strukturdefinitionen, jedoch mit demselben Versatz. Programmierer nutzten diese Fähigkeit für eine Vielzahl von Zwecken.
Wenn Strukturelementen ihre eigenen Namespaces zugewiesen wurden, war es unmöglich, zwei Strukturelemente mit derselben Verschiebung zu deklarieren. Das Hinzufügen von Gewerkschaften zur Sprache ermöglichte es, dieselbe Semantik zu erreichen, die in früheren Versionen der Sprache verfügbar war (obwohl die Unfähigkeit, Namen in einen umschließenden Kontext exportieren zu lassen, möglicherweise immer noch die Verwendung eines Find / Replace zum Ersetzen von foo-> member erforderlich gemacht hat in foo-> type1.member). Was wichtig war, war nicht so sehr, dass die Leute, die Gewerkschaften hinzugefügt haben, eine bestimmte Zielverwendung im Auge haben, sondern dass sie ein Mittel darstellen, mit dem Programmierer, die sich für welchen Zweck auch immer auf die frühere Semantik verlassen hatten, in der Lage sein sollten, dies zu erreichen gleiche Semantik, auch wenn sie dafür eine andere Syntax verwenden mussten.
quelle
Sie können verwenden aa Vereinigung aus zwei Gründen:
1 Ist wirklich eher ein C-Hack, um Code zu verkürzen, wenn Sie wissen, wie die Speicherarchitektur des Zielsystems funktioniert. Wie bereits gesagt, können Sie normalerweise damit durchkommen, wenn Sie nicht auf viele verschiedene Plattformen abzielen. Ich glaube, einige Compiler lassen Sie möglicherweise auch Packanweisungen verwenden (ich weiß, dass sie dies für Strukturen tun).
Ein gutes Beispiel für 2. ist der Typ VARIANT, der in COM häufig verwendet wird.
quelle
Wie bereits erwähnt, können Gewerkschaften in Kombination mit Aufzählungen und in Strukturen eingeschlossen werden, um markierte Gewerkschaften zu implementieren. Eine praktische Anwendung ist die Implementierung von Rusts
Result<T, E>
, die ursprünglich mit einem reinen implementiert wurdenenum
(Rust kann zusätzliche Daten in Aufzählungsvarianten enthalten). Hier ist ein C ++ - Beispiel:quelle