Zweck der Gewerkschaften in C und C ++

254

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.

legends2k
quelle
11
Einfach ausgedrückt, Compiler dürfen zwischen Elementen in einer Struktur Auffüllungen einfügen. Somit b, g, r,und akann nicht zusammenhängend sein und somit nicht mit dem Layout von a übereinstimmen uint32_t. Dies ist zusätzlich zu den Endianess-Problemen, auf die andere hingewiesen haben.
Thomas Matthews
8
Genau aus diesem Grund sollten Sie die Fragen C und C ++ nicht mit Tags versehen. Die Antworten sind unterschiedlich, aber da die Antwortenden nicht einmal sagen, für welches Tag sie antworten (wissen sie es überhaupt?), Erhalten Sie Müll.
Pascal Cuoq
5
@downvoter Danke, dass du es nicht erklärt hast. Ich verstehe, dass du willst, dass ich deine Beschwerden magisch verstehe und sie in Zukunft nicht wiederhole: P
legends2k
1
Bedenken Sie in Bezug auf die ursprüngliche Absicht, eine Gewerkschaft zu gründen , dass der C-Standard die Gewerkschaften um mehrere Jahre nachdatiert. Ein kurzer Blick auf Unix V7 zeigt einige Typkonvertierungen über Gewerkschaften.
Ninjalj
3
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 "
underscore_d

Antworten:

407

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 undefiniertes Verhalten in C89 / 90 als implementierungsdefiniertes Verhalten beschrieben 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.

Ameise
quelle
37
+1 für die Ausarbeitung, ein einfaches praktisches Beispiel und das Sprichwort über das Erbe der Gewerkschaften!
Legends2k
6
Das Problem, das ich mit dieser Antwort habe, ist, dass die meisten Betriebssysteme, die ich gesehen habe, Header-Dateien haben, die genau das tun. Zum Beispiel habe ich es in alten (vor 64-Bit) Versionen von <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.
TED
31
@AndreyT „Es war bis vor kurzem noch nie legal, Gewerkschaften für Typ-Punning zu verwenden“: 2004 ist nicht „sehr neu“, insbesondere angesichts der Tatsache, dass ursprünglich nur C99 ungeschickt formuliert wurde und das Typ-Punning durch Gewerkschaften undefiniert zu sein scheint. In Wirklichkeit ist das Typ-Punning durch Gewerkschaften in C89 legal, in C11 legal, und es war in C99 die ganze Zeit legal, obwohl es bis 2004 dauerte, bis das Komitee falsche Formulierungen und die anschließende Veröffentlichung von TC3 korrigierte. open-std.org/jtc1/sc22/wg14/www/docs/dr_283.htm
Pascal Cuoq
6
@ legends2k Die Programmiersprache ist standardmäßig definiert. Die Technische Berichtigung 3 der Norm C99 erlaubt in ihrer Fußnote 82 ausdrücklich die Eingabe von Schriftzeichen, die ich Sie einladen möchte, selbst zu lesen. Dies ist kein Fernsehen, in dem Rockstars interviewt werden und ihre Meinung zum Klimawandel äußern. Die Meinung von Stroustrup hat keinen Einfluss darauf, was der C-Standard sagt.
Pascal Cuoq
6
@ legends2k " Ich weiß, dass die Meinung eines Einzelnen keine Rolle spielt und nur der Standard " Die Meinung von Compiler-Autoren ist viel wichtiger als die (extrem schlechte) Sprachspezifikation.
Neugieriger
38

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:

struct VAROBJECT
{
    enum o_t { Int, Double, String } objectType;

    union
    {
        int intValue;
        double dblValue;
        char *strValue;
    } value;
} object;
Erich Kitzmüller
quelle
Ich stimme vollkommen zu, ohne in das Chaos des undefinierten Verhaltens einzutreten. Vielleicht ist dies das am besten beabsichtigte Verhalten der Gewerkschaften, an das ich denken kann. aber nicht wird Platz verschwenden, wenn ich nur, sagen wir, intoder char*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?
Legends2k
3
Legenden: In manchen Fällen kann man das einfach nicht. Sie verwenden in den gleichen Fällen etwas wie VAROBJECT in C, wenn Sie Object in Java verwenden.
Erich Kitzmüller
Die Datenstruktur von getaggten Gewerkschaften scheint, wie Sie erklären, eine nur legitime Verwendung von Gewerkschaften zu sein.
Legends2k
Geben Sie auch ein Beispiel für die Verwendung der Werte.
Ciro Santilli 4 冠状 病 六四 事件 4
1
@CiroSantilli part 改造 中心 六四 六四 help Ein Teil eines Beispiels aus C ++ Primer könnte hilfreich sein. wandbox.org/permlink/cFSrXyG02vOSdBk2
Rick
34

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_tdie 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.

David Rodríguez - Dribeas
quelle
+1 für die klare und einfache Antwort! Ich bin damit einverstanden, dass aus Gründen der Portabilität die Methode, die Sie im 2. Absatz angegeben haben, gilt. Aber kann ich die Art und Weise verwenden, wie ich sie in der Frage formuliert habe, wenn mein Code an eine einzelne Architektur gebunden ist (wobei der Preis für die Schutzfähigkeit bezahlt wird), da 4 Bytes für jeden Pixelwert und einige Zeit beim Ausführen dieser Funktion eingespart werden? ?
Legends2k
Das Endian-Problem zwingt den Standard nicht dazu, es als undefiniertes Verhalten zu deklarieren. Reinterpret_cast weist genau dieselben Endian-Probleme auf, hat jedoch ein implementierungsdefiniertes Verhalten.
JoeG
1
@ legends2k, das Problem ist, dass das Optimierungsprogramm möglicherweise davon ausgeht, dass ein uint32_t nicht durch Schreiben in ein uint8_t geändert wird, und Sie daher den falschen Wert erhalten, wenn der Optimierte diese Annahme verwendet ... @Joe, das undefinierte Verhalten wird angezeigt, sobald Sie auf das zugreifen Zeiger (ich weiß, es gibt einige Ausnahmen).
AProgrammer
1
@ legends2k / AProgrammer: Das Ergebnis eines reinterpret_cast ist eine definierte Implementierung. Die Verwendung des zurückgegebenen Zeigers führt nicht zu undefiniertem Verhalten, sondern nur zu implementierungsdefiniertem Verhalten. Mit anderen Worten, das Verhalten muss konsistent und definiert sein, ist jedoch nicht portierbar.
JoeG
1
@ legends2k: Jeder anständige Optimierer erkennt bitweise Operationen, die ein ganzes Byte auswählen und Code zum Lesen / Schreiben des Bytes generieren, genau wie die Union, aber gut definiert (und portabel). zB uint8_t getRed () const {return color & 0x000000FF; } void setRed (uint8_t r) {color = (color & ~ 0x000000FF) | r; }
Ben Voigt
22

Die häufigste Verwendung, auf die unionich regelmäßig stoße, ist Aliasing .

Folgendes berücksichtigen:

union Vector3f
{
  struct{ float x,y,z ; } ;
  float elts[3];
}

Was macht das? Es ermöglicht einen sauberen und übersichtlichen Zugriff auf Vector3f vec;die Mitglieder eines der beiden Namen:

vec.x=vec.y=vec.z=1.f ;

oder durch ganzzahligen Zugriff auf das Array

for( int i = 0 ; i < 3 ; i++ )
  vec.elts[i]=1.f;

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.

Bobobobo
quelle
3
Dies wird auch genannt, type-punningwas auch in der Frage erwähnt wird. Auch das Beispiel in der Frage zeigt ein ähnliches Beispiel.
Legends2k
4
Es ist kein Typ Punning. In meinem Beispiel stimmen die Typen überein , sodass es kein "Wortspiel" gibt, sondern lediglich ein Aliasing.
Bobobobo
3
Ja, aber aus absoluter Sicht des Sprachstandards sind die Mitglieder, in die geschrieben und aus denen gelesen wird, unterschiedlich, was nicht definiert ist, wie in der Frage erwähnt.
Legends2k
3
Ich würde hoffen, dass ein zukünftiger Standard diesen speziellen Fall so regelt, dass er nach der Regel "gemeinsame anfängliche Teilsequenz" zulässig ist. Arrays nehmen jedoch unter dem aktuellen Wortlaut nicht an dieser Regel teil.
Ben Voigt
3
@curiousguy: Es ist eindeutig nicht erforderlich, dass die Strukturelemente ohne willkürliche Polsterung platziert werden. Wenn Code auf Platzierung von Strukturelementen oder Strukturgröße testet, sollte Code funktionieren, wenn Zugriffe direkt über die Union erfolgen. Eine strikte Lektüre des Standards würde jedoch darauf hinweisen, dass die Verwendung der Adresse eines Union- oder Strukturelements einen Zeiger ergibt, der nicht verwendet werden kann als Zeiger seines eigenen Typs, muss jedoch zuerst wieder in einen Zeiger auf den umschließenden Typ oder einen Zeichentyp konvertiert werden. Jeder fernbedienbare Compiler erweitert die Sprache, indem er mehr Dinge zum
Laufen bringt
10

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.

union A {
   int i;
   double d;
};

A a[10];    // records in "a" can be either ints or doubles 
a[0].i = 42;
a[1].d = 1.23;

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
Hast du es so benutzt (wie in der Frage) ?? :)
legends2k
Es ist ein bisschen pedantisch, aber ich akzeptiere "Variantenaufzeichnungen" nicht ganz. Das heißt, ich bin sicher, sie waren im Sinn, aber wenn sie eine Priorität waren, warum nicht sie zur Verfügung stellen? "Stellen Sie den Baustein bereit, weil es nützlich sein könnte, auch andere Dinge zu bauen", scheint nur intuitiv wahrscheinlicher. Insbesondere bei mindestens einer weiteren Anwendung, die wahrscheinlich in Betracht gezogen wurde - speicherabgebildete E / A-Register, bei denen die Eingabe- und Ausgaberegister (während sie sich überlappen) unterschiedliche Entitäten mit ihren eigenen Namen, Typen usw. sind
Steve314
@ Stev314 Wenn das die Verwendung war, die sie im Sinn hatten, hätten sie es nicht zu einem undefinierten Verhalten machen können.
@Neil: +1 für den ersten, der über die tatsächliche Verwendung spricht, ohne auf undefiniertes Verhalten zu stoßen. Ich denke, sie hätten die Implementierung wie andere Typ-Punning-Operationen (reinterpret_cast usw.) definieren können. Aber wie ich gefragt habe, haben Sie es zum Typ-Punning verwendet?
Legends2k
@Neil - das Beispiel für ein Speicher-zugeordnetes Register ist nicht undefiniert, das übliche Endian / etc beiseite und mit einem "flüchtigen" Flag versehen. Das Schreiben an eine Adresse in diesem Modell verweist nicht auf dasselbe Register wie das Lesen derselben Adresse. Daher gibt es kein Problem mit dem Thema "Was lesen Sie zurück?", Da Sie nicht zurücklesen. Unabhängig davon, welche Ausgabe Sie an diese Adresse geschrieben haben, lesen Sie beim Lesen nur eine unabhängige Eingabe. Das einzige Problem besteht darin, sicherzustellen, dass Sie die Eingabeseite der Union lesen und die Ausgabeseite schreiben. War in eingebetteten Sachen üblich - wahrscheinlich immer noch.
Steve314
8

In C war es eine gute Möglichkeit, so etwas wie eine Variante zu implementieren.

enum possibleTypes{
  eInt,
  eDouble,
  eChar
}


struct Value{

    union Value {
      int iVal_;
      double dval;
      char cVal;
    } value_;
    possibleTypes discriminator_;
} 

switch(val.discriminator_)
{
  case eInt: val.value_.iVal_; break;

In Zeiten mit wenig Speicher benötigt diese Struktur weniger Speicher als eine Struktur mit allen Mitgliedern.

Übrigens bietet C.

    typedef struct {
      unsigned int mantissa_low:32;      //mantissa
      unsigned int mantissa_high:20;
      unsigned int exponent:11;         //exponent
      unsigned int sign:1;
    } realVal;

um auf Bitwerte zuzugreifen.

Totonga
quelle
Obwohl beide Beispiele im Standard perfekt definiert sind; Aber hey, die Verwendung von Bitfeldern ist sicher ein nicht portierbarer Code, nicht wahr?
Legends2k
Nein, ist es nicht. Soweit ich weiß, wird es weitgehend unterstützt.
Totonga
1
Die Compiler-Unterstützung wird nicht portabel. Das C-Buch : C (damit C ++) gibt keine Garantie für die Reihenfolge von Feldern innerhalb von Maschinenwörtern. Wenn Sie sie also aus letzterem Grund verwenden, ist Ihr Programm nicht nur nicht portierbar, sondern auch vom Compiler abhängig.
Legends2k
5

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.

Paul R.
quelle
2
Gibt es nicht ein Endian-Problem? Eine relativ einfache Lösung im Vergleich zu "undefiniert", aber es lohnt sich, einige Projekte zu berücksichtigen, wenn dies der Fall ist.
Steve314
5

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 + unionKonstrukt (Stapel auch zugewiesen usw.), aber es verwendet eine Vorlagenliste von Typen anstelle der enum:)

Matthieu M.
quelle
5

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).

Nick
quelle
4

Technisch gesehen ist es undefiniert, aber in Wirklichkeit behandeln die meisten (alle?) Compiler es genauso wie die Verwendung eines reinterpret_castvon einem Typ zum anderen, dessen Ergebnis die Implementierung definiert ist. Ich würde nicht den Schlaf über Ihren aktuellen Code verlieren.

JoeG
quelle
" Ein reinterpret_cast von einem Typ zum anderen, dessen Ergebnis die Implementierung definiert ist. " Nein, das ist es nicht. Implementierungen müssen es nicht definieren, und die meisten definieren es nicht. Was wäre das zulässige implementierungsdefinierte Verhalten beim Umwandeln eines zufälligen Werts in einen Zeiger?
Neugieriger
4

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.

Cubbi
quelle
4

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].

philcolbourn
quelle
Ein Byte, das wortausgerichtet sein muss? Das macht keinen Sinn. Ein Byte hat per Definition keine Ausrichtungsanforderung.
Neugieriger
Ja, ich hätte wahrscheinlich ein besseres Beispiel verwenden sollen. Vielen Dank.
Philcolbourn
@curiousguy: Es gibt viele Fälle, in denen Arrays von Bytes wortausgerichtet werden sollen. Wenn man viele Arrays von z. B. 1024 Bytes hat und häufig eines in ein anderes kopieren möchte, kann die Wortausrichtung auf vielen Systemen die Geschwindigkeit von a memcpy()von einem zum anderen verdoppeln . Einige Systeme können char[]Zuweisungen , die außerhalb von Strukturen / Gewerkschaften aus diesem und anderen Gründen auftreten, spekulativ ausrichten . Im vorliegenden Beispiel ist die Annahme, dass isich alle Elemente von überlappen, c[]nicht portierbar, aber das liegt daran, dass es keine Garantie dafür gibt sizeof(int)==4.
Supercat
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.

Superkatze
quelle
Schätzen Sie die Geschichtsstunde, aber mit dem Standard, der solche und solche wie undefiniert definiert, was in der vergangenen C-Ära, in der das K & R-Buch der einzige "Standard" war, nicht der Fall war, muss man sicher sein, dass es nicht für irgendeinen Zweck verwendet wird und betritt das UB-Land.
Legends2k
2
@ legends2k: Als der Standard geschrieben wurde, behandelten die meisten C-Implementierungen die Gewerkschaften auf die gleiche Weise, und eine solche Behandlung war nützlich. Einige jedoch nicht, und die Autoren des Standards waren abgeneigt, vorhandene Implementierungen als "nicht konform" zu kennzeichnen. Stattdessen stellten sie fest, dass, wenn Implementierer den Standard nicht benötigen, um ihnen zu sagen, dass sie etwas tun sollen (was durch die Tatsache belegt wird, dass sie es bereits tun ), der Status quo einfach erhalten bleibt , wenn er nicht spezifiziert oder undefiniert bleibt . Die Vorstellung, dass es die Dinge weniger definieren sollte als vor dem Standard, wurde geschrieben ...
Supercat
2
... scheint eine viel neuere Innovation zu sein. Was an all dem besonders traurig ist, ist, dass Compiler-Autoren, die auf High-End-Anwendungen abzielen, herausfinden sollten, wie sie der Sprache, die die meisten Compiler in den 1990er Jahren implementiert haben, nützliche Optimierungsanweisungen hinzufügen können, anstatt Funktionen und Garantien zu entkernen, die nur von "unterstützt" wurden "90% der Implementierungen, das Ergebnis wäre eine Sprache, die besser und zuverlässiger als
hypermodernes
2

Sie können verwenden aa Vereinigung aus zwei Gründen:

  1. Eine praktische Möglichkeit, auf unterschiedliche Weise auf dieselben Daten zuzugreifen, wie in Ihrem Beispiel
  2. Eine Möglichkeit, Platz zu sparen, wenn es verschiedene Datenelemente gibt, von denen nur eines jemals "aktiv" sein kann.

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.

Mr. Boy
quelle
2

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 wurden enum(Rust kann zusätzliche Daten in Aufzählungsvarianten enthalten). Hier ist ein C ++ - Beispiel:

template <typename T, typename E> struct Result {
    public:
    enum class Success : uint8_t { Ok, Err };
    Result(T val) {
        m_success = Success::Ok;
        m_value.ok = val;
    }
    Result(E val) {
        m_success = Success::Err;
        m_value.err = val;
    }
    inline bool operator==(const Result& other) {
        return other.m_success == this->m_success;
    }
    inline bool operator!=(const Result& other) {
        return other.m_success != this->m_success;
    }
    inline T expect(const char* errorMsg) {
        if (m_success == Success::Err) throw errorMsg;
        else return m_value.ok;
    }
    inline bool is_ok() {
        return m_success == Success::Ok;
    }
    inline bool is_err() {
        return m_success == Success::Err;
    }
    inline const T* ok() {
        if (is_ok()) return m_value.ok;
        else return nullptr;
    }
    inline const T* err() {
        if (is_err()) return m_value.err;
        else return nullptr;
    }

    // Other methods from https://doc.rust-lang.org/std/result/enum.Result.html

    private:
    Success m_success;
    union _val_t { T ok; E err; } m_value;
}
Kotauskas
quelle