Gewerkschaften und Typ-Punning

76

Ich habe eine Weile gesucht, kann aber keine klare Antwort finden.

Viele Leute sagen, dass die Verwendung von Gewerkschaften zum Schreiben von Wortspielen undefiniert und eine schlechte Praxis ist. Warum ist das? Ich kann keinen Grund erkennen, warum es etwas Undefiniertes tun würde, wenn man bedenkt, dass sich der Speicher, in den Sie die Originalinformationen schreiben, nicht von selbst ändert (es sei denn, es liegt außerhalb des Gültigkeitsbereichs des Stapels, aber das ist kein Gewerkschaftsproblem , das wäre schlechtes Design).

Die Leute zitieren die strenge Aliasing-Regel, aber das scheint mir so, als würde man sagen, dass man es nicht kann, weil man es nicht kann.

Was ist der Sinn einer Gewerkschaft, wenn sie kein Wortspiel tippt? Ich habe irgendwo gesehen, dass sie verwendet werden sollen, um denselben Speicherort für unterschiedliche Informationen zu unterschiedlichen Zeiten zu verwenden, aber warum nicht einfach die Informationen löschen, bevor sie erneut verwendet werden?

Zusammenfassen:

  1. Warum ist es schlecht, Gewerkschaften für Typ Punning zu verwenden?
  2. Was ist der Sinn von ihnen, wenn nicht das?

Zusätzliche Informationen: Ich verwende hauptsächlich C ++, möchte aber darüber und über C Bescheid wissen. Insbesondere verwende ich Gewerkschaften, um zwischen Floats und dem rohen Hex zu konvertieren und über den CAN-Bus zu senden.

Matthew Wilkins
quelle
1
Denken Sie für eine sehr häufige Verwendung von Gewerkschaften an den lexikalischen Analysator in einem Compiler. Es kann ein Token-Wert-Paar an den Parser zurückgeben, und abhängig vom Token kann der Wert beispielsweise entweder eine Ganzzahl, eine Gleitkommazahl, ein Zeichen oder ein Zeiger auf eine Zeichenfolge sein. Wie würden Sie diese verschiedenen Werttypen am besten in einer einzigen Struktur darstellen? Eine Vereinigung natürlich.
Einige Programmierer Typ
1
In meiner Antwort auf Warum beendet die Optimierung diese Funktion? Erläutert ich in C und C ++, ob das Typ-Punning über eine Union sowohl in C als auch in C ++ legal ist . . Grundsätzlich ist in C immer legal, nicht klar, ob es in C ++ legal ist, aber in der Praxis unterstützen die meisten Compiler es in C ++.
Shafik Yaghmour
Ich wollte vor einiger Zeit eine Antwort darauf hinzufügen, habe es aber vergessen und bin dann wieder auf diese Frage gestoßen, als ich mich mit etwas anderem befasst habe. Nun, ich habe gerade meine Antwort hinzugefügt.
Shafik Yaghmour

Antworten:

50

Um es noch einmal zu wiederholen, ist es in C (aber nicht in C ++) vollkommen in Ordnung, durch Gewerkschaften zu tippen. Im Gegensatz dazu verstößt die Verwendung von Zeigerumwandlungen gegen das strikte C99-Aliasing und ist problematisch, da unterschiedliche Typen unterschiedliche Ausrichtungsanforderungen haben können und Sie einen SIGBUS auslösen können, wenn Sie es falsch machen. Bei Gewerkschaften ist dies nie ein Problem.

Die relevanten Zitate aus den C-Standards sind:

C89 Abschnitt 3.3.2.3 §5:

Wenn auf ein Mitglied eines Vereinigungsobjekts zugegriffen wird, nachdem ein Wert in einem anderen Mitglied des Objekts gespeichert wurde, ist das Verhalten implementierungsdefiniert

C11 Abschnitt 6.5.2.3 §3:

Ein Postfix-Ausdruck gefolgt von. Operator und Bezeichner bezeichnen ein Mitglied einer Struktur oder eines Vereinigungsobjekts. Der Wert ist der des benannten Mitglieds

mit folgender Fußnote 95:

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

Dies sollte völlig klar sein.


James ist verwirrt, weil C11 Abschnitt 6.7.2.1 §16 lautet

Der Wert von höchstens einem der Mitglieder kann jederzeit in einem Gewerkschaftsobjekt gespeichert werden.

Dies scheint widersprüchlich, ist es aber nicht: Im Gegensatz zu C ++ gibt es in C kein Konzept für ein aktives Mitglied und es ist vollkommen in Ordnung, über einen Ausdruck eines inkompatiblen Typs auf den einzelnen gespeicherten Wert zuzugreifen.

Siehe auch C11 Anhang J.1 §1:

Die Werte von Bytes, die anderen Gewerkschaftsmitgliedern als dem zuletzt in [gespeicherten entsprechen, sind nicht angegeben].

In C99 wurde dies früher gelesen

Der Wert eines anderen Gewerkschaftsmitglieds als des zuletzt in [gespeicherten] ist nicht angegeben.

Das war falsch. Da der Anhang nicht normativ ist, hat er seinen eigenen TC nicht bewertet und musste bis zur nächsten Standardrevision warten, um behoben zu werden.


GNU-Erweiterungen auf Standard-C ++ (und auf C90) erlauben explizit Typ-Punning mit Gewerkschaften . Andere Compiler, die keine GNU-Erweiterungen unterstützen, unterstützen möglicherweise auch Union-Type-Punning, sind jedoch nicht Teil des Basissprachenstandards.

Christoph
quelle
2
Ich habe meine Kopie von C90 nicht zur Hand, um den Kontext zu überprüfen. Ich erinnere mich aus den Ausschussdiskussionen, dass eine der Absichten darin bestand, dass der Wortlaut "Debugging" -Implementierungen ermöglichen sollte, die eingeschlossen wurden, wenn der Zugriff nicht das letzte geschriebene Element war. (Dies war natürlich in den späten 1980er Jahren; die Haltung des C-Komitees hat sich möglicherweise seitdem weiterentwickelt.) Ich erinnere mich, dass dies durch undefiniertes Verhalten erreicht wurde, aber die Implementierung würde auch den Trick tun. (Der Hauptunterschied besteht darin, dass die Implementierung erforderlich wäre, um zu dokumentieren, was sie tut.)
James Kanze
4
Die Fußnote ist nicht normativ und im Kontext eindeutig eine Erklärung dafür, warum der Ausschuss dies nicht definiert hat. Es definiert kein Verhalten.
James Kanze
4
@ JamesKanze: Der Wert ist der des benannten Mitglieds . Das ist der normative Teil, der in der Fußnote klargestellt wird. Wenn alle Bytes, aus denen die Objektdarstellung dieses Elements besteht, den angegebenen Wert annehmen und keiner Trap-Darstellung entsprechen, nimmt das Element ebenfalls den angegebenen Wert an. Es spielt keine Rolle, wie diese Bytes dorthin memcpygelangt sind ( durch Modifikation über char *, durch ein anderes Gewerkschaftsmitglied, ...). Sie werden mich nicht anders überzeugen können. Wenn Sie es sich nicht anders überlegen, ist es wahrscheinlich sinnlos, fortzufahren ...
Christoph
1
Ich erinnere mich aus den Ausschussdiskussionen, dass eine der Absichten darin bestand, dass der Wortlaut "Debugging" -Implementierungen ermöglichen sollte, die eingeschlossen wurden, wenn der Zugriff nicht das letzte geschriebene Element war. Das könnte in den 80er Jahren der Fall gewesen sein; Als C99 das Typ-Punting durch Zeiger-Casts nicht erlaubte, entstand die Notwendigkeit eines anderen Mechanismus. Das ist es; Leider scheint es in der C99-Begründung keine Erwähnung zu geben, aber es ist plausibel, dass dies geschah
Christoph
1
Auch ich habe den Eindruck, dass sich die Haltung des C-Ausschusses weiterentwickelt hat; Seit 1990 bin ich an der Standardisierung von C ++ beteiligt und habe C nicht so genau verfolgt. Die Grundregel gilt jedoch weiterhin: Alles, was der Standard nicht definiert, ist undefiniertes Verhalten. Und das fällt eindeutig in diese Kategorie. Ich denke (kann aber nicht beweisen), dass die Absicht ist, dass alle Typ-Punning undefiniertes Verhalten sind, das durch die Implementierung definiert wird.
James Kanze
17

Der ursprüngliche Zweck der Gewerkschaften bestand darin, Platz zu sparen, wenn Sie in der Lage sein möchten, verschiedene Typen darzustellen. Dies wird als Variantentyp bezeichnet. Siehe Boost.Variant als gutes Beispiel dafür.

Die andere gebräuchliche Verwendung ist Typ-Punning. Die Gültigkeit dieses Dokuments wird diskutiert, aber praktisch die meisten Compiler unterstützen es. Wir können sehen, dass gcc seine Unterstützung dokumentiert :

Die Praxis, von einem anderen Gewerkschaftsmitglied zu lesen als dem, an das zuletzt geschrieben wurde („Typ-Punning“ genannt), ist üblich. Selbst bei -fstrict-aliasing ist Typ-Punning zulässig, vorausgesetzt, auf den Speicher wird über den Union-Typ zugegriffen. Der obige Code funktioniert also wie erwartet.

Beachten Sie, dass auch bei -fstrict-aliasing Typ-Punning zulässig ist, was darauf hinweist, dass ein Aliasing-Problem vorliegt .

Pascal Cuoq hat argumentiert, dass der Fehlerbericht 283 klarstellte, dass dies in C zulässig war. Der Fehlerbericht 283 fügte die folgende Fußnote als Klarstellung hinzu:

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 Typ als neu interpretiert beschrieben in 6.2.6 (ein Prozess, der manchmal als "Typ Punning" bezeichnet wird). Dies könnte eine Trap-Darstellung sein.

in C11 wäre das eine Fußnote 95.

Obwohl im std-discussionMail-Gruppenthema Typ Punning über eine Union das Argument vorgebracht wird, ist dies unterbestimmt, was vernünftig erscheint, da DR 283kein neuer normativer Wortlaut hinzugefügt wurde, sondern nur eine Fußnote:

Dies ist meiner Meinung nach ein unterbestimmter semantischer Sumpf in C. Zwischen den Implementierern und dem C-Ausschuss wurde kein Konsens darüber erzielt, welche Fälle genau das Verhalten definiert haben und welche nicht [...]

In C ++ ist unklar, ob Verhalten definiert ist oder nicht .

Diese Diskussion behandelt auch mindestens einen Grund, warum es unerwünscht ist, Typ-Punning durch eine Gewerkschaft zuzulassen:

[...] Die Regeln des C-Standards brechen die typbasierten Alias-Analyseoptimierungen, die aktuelle Implementierungen durchführen.

es bricht einige Optimierungen. Das zweite Argument dagegen ist, dass die Verwendung von memcpy identischen Code generieren sollte und Optimierungen und genau definiertes Verhalten nicht beeinträchtigt, zum Beispiel Folgendes:

std::int64_t n;
std::memcpy(&n, &d, sizeof d);

an Stelle von:

union u1
{
  std::int64_t n;
  double d ;
} ;

u1 u ;
u.d = d ;

und wir können sehen, dass mit godbolt identischer Code generiert wird und das Argument wird gemacht, wenn Ihr Compiler keinen identischen Code generiert, sollte dies als Fehler betrachtet werden:

Wenn dies für Ihre Implementierung zutrifft, empfehle ich Ihnen, einen Fehler zu melden. Es scheint mir eine schlechte Idee zu sein, echte Optimierungen (alles, was auf typbasierter Alias-Analyse basiert) zu brechen, um Leistungsprobleme mit einem bestimmten Compiler zu umgehen.

Der Blog-Beitrag Type Punning, Strict Aliasing und Optimization kommt ebenfalls zu einem ähnlichen Ergebnis.

Die Diskussion über undefiniertes Verhalten in der Mailingliste: Geben Sie punning ein, um das Kopieren zu vermeiden .

Shafik Yaghmour
quelle
1
Die Behauptung, dass memcpy identischen Code erzeugt, ignoriert die Tatsache, dass effizienterer Code generiert werden könnte, wenn Compiler Muster dokumentieren würden, bei denen Aliasing erkannt würde. In diesem Fall müsste ein Compiler nur eine kleine Anzahl von leicht pessimistischen (aber wahrscheinlich genauen) Codes erstellen. Vermutungen, während memcpy einen Compiler häufig dazu zwingt, pessimistischere Vermutungen anzustellen. Der Code für memcpy selbst mag gut aussehen, aber seine Auswirkungen auf den Code um ihn herum sind nicht so sehr.
Supercat
Es ist erwähnenswert, dass wir mit C ++ 17 std::variantals Variantentyp bekommen
Justin
2
Es könnte auch gut sein zu erwähnen, dass dies std::memcpynur gültig ist, wenn die Typen TriviallyCopyable sind
Justin
@supercat Wenn Sie ein Godbolt-Beispiel bereitstellen könnten, das diesen Effekt zeigt, wäre dies sehr hilfreich. Soweit ich Richards Position verstehe, sollte dies nicht der Fall sein, vielleicht ist es dann ein Fehler.
Shafik Yaghmour
@ShafikYaghmour: Angesichts des Codes uint16_t *outptr; void store_double_halfword(uint32_t dat) { uint32_t *dp = (uint32_t*)outptr; outptr = dp+1; memcpy(dp, &dat, sizeof (uint32_t)); } void store_loop1(uint32_t *src){ for (int i=0; i<100; i++) store_next_word1(src[i]); }gibt es für einen Compiler keine Möglichkeit, zu vermeiden, dass er outptrbei jedem Durchlauf durch die Schleife neu laden und neu speichern muss, wenn Code verwendet wird memcpy. Wenn man sich darauf verlassen könnte, dass der Compiler die Besetzung uint16_t*als Zeichen dafür behandelt, dass die Funktion auf Dinge vom Typ zugreifen könnte uint16_toder uint32_tCode
zulässt
6

Es ist legal in C99:

Aus dem Standard: 6.5.2.3 Struktur und Gewerkschaftsmitglieder

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 Typ als neu interpretiert beschrieben in 6.2.6 (ein Prozess, der manchmal als "Typ Punning" bezeichnet wird). Dies könnte eine Trap-Darstellung sein.

David Ranieri
quelle
6
@JamesKanze Könnten Sie näher erläutern, wie „der entsprechende Teil der Objektdarstellung des Werts als Objektdarstellung im neuen Typ neu interpretiert wird, wie in 6.2.6 beschrieben (ein Prozess, der manchmal als„ Typ-Punning “bezeichnet wird). Dies könnte eine Fallendarstellung sein. “Ist eine ausgefallene Art zu sagen, dass es sich um undefiniertes Verhalten handelt? Es scheint mir, dass das Gelesene eine Neuinterpretation des neuen Typs ist und dass dies eine ausgefallene Art zu sagen ist, dass es sich , wenn überhaupt , um ein implementierungsdefiniertes Verhalten handelt.
Pascal Cuoq
@PascalCuoq Alles, was zu einer Falle führen kann, ist undefiniertes Verhalten.
James Kanze
8
@JamesKanze Ich nehme "Dies könnte eine Trap-Darstellung sein", um zu bedeuten, dass, wenn der neue Typ Trap-Darstellungen hat, unter implementierungsdefinierten Bedingungen das Ergebnis der Typ-Punning eines davon sein kann.
Pascal Cuoq
1
@JamesKanze: Das Typ-Punning über Gewerkschaften ist genau definiert, solange es nicht zu einer Trap-Darstellung führt (und der Quelltyp nicht kleiner als der Zieltyp ist). Dies ist eine Einzelfallentscheidung in Abhängigkeit von den beteiligten Typen und Werten . In C99 gibt es eine Fußnote, die sehr deutlich macht, dass Typ-Punning legal ist. Der (nicht normative!) Anhang führte es fälschlicherweise als nicht spezifiziertes ( nicht undefiniertes) Verhalten auf. Der Anhang wurde mit C11
Christoph
1
@ JamesKanze: Ja, das gilt nur für C; Die Verwendung von Gewerkschaften auf diese Weise war jedoch nie ein undefiniertes Verhalten. siehe C89-Entwurf, Abschnitt 3.3.2.3: Wenn auf ein Mitglied eines Vereinigungsobjekts zugegriffen wird, nachdem ein Wert in einem anderen Mitglied des Objekts gespeichert wurde, ist das Verhalten implementierungsdefiniert
Christoph
4

KURZE ANTWORT: Typ Punning kann unter bestimmten Umständen sicher sein. Auf der anderen Seite scheint es, obwohl es eine sehr bekannte Praxis zu sein scheint, dass Standard nicht sehr daran interessiert ist, sie offiziell zu machen.

Ich werde nur über C sprechen (nicht über C ++).

1. TYP PUNNING und DIE STANDARDS

Wie bereits erwähnt, ist das Typ-Punning im Standard C99 und auch in C11 in Unterabschnitt 6.5.2.3 zulässig . Ich werde jedoch Fakten mit meiner eigenen Wahrnehmung des Problems umschreiben:

  • In Abschnitt 6.5 der Standarddokumente C99 und C11 wird das Thema Ausdrücke behandelt .
  • Der Unterabschnitt 6.5.2 bezieht sich auf Postfix-Ausdrücke .
  • Der Unterabschnitt 6.5.2.3 befasst sich mit Strukturen und Gewerkschaften .
  • In Abschnitt 6.5.2.3 (3) wird erläutert , welcher Punktoperator auf ein structoder ein unionObjekt angewendet wird und welcher Wert erhalten wird.
    Genau dort erscheint die Fußnote 95 . Diese Fußnote sagt:

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 Typ als neu interpretiert beschrieben in 6.2.6 (ein Prozess, der manchmal als "Typ Punning" bezeichnet wird). Dies könnte eine Trap-Darstellung sein.

Die Tatsache, dass Typ-Punning kaum auftritt, und als Fußnote gibt es einen Hinweis darauf, dass es sich bei der C-Programmierung nicht um ein relevantes Problem handelt.
Tatsächlich besteht der Hauptzweck der Verwendung unionsdarin, Platz (im Speicher) zu sparen . Da mehrere Mitglieder dieselbe Adresse verwenden, kann, wenn man weiß, dass jedes Mitglied unterschiedliche Teile des Programms verwendet, niemals gleichzeitig, unionstattdessen a structzum Speichern von Speicher verwendet werden.

  • Der Unterabschnitt 6.2.6 wird erwähnt.
  • In Unterabschnitt 6.2.6 wird erläutert, wie Objekte dargestellt werden (z. B. im Speicher).

2. VERTRETUNG DER TYPEN UND IHRER STÖRUNGEN

Wenn Sie auf die verschiedenen Aspekte des Standards achten, können Sie sich fast nichts sicher sein:

  • Die Darstellung von Zeigern ist nicht eindeutig festgelegt.
  • Am schlimmsten ist, dass Zeiger mit unterschiedlichen Typen eine unterschiedliche Darstellung haben können (als Objekte im Speicher).
  • unionMitglieder haben dieselbe Überschriftenadresse im Speicher und dieselbe Adresse wie das unionObjekt selbst.
  • structMitglieder haben eine zunehmende relative Adresse, indem sie in genau derselben Speicheradresse beginnen wie das structObjekt selbst. Am Ende jedes Mitglieds können jedoch Füllbytes hinzugefügt werden. Wie viele? Es ist unvorhersehbar. Füllbytes werden hauptsächlich für Speicherzuweisungszwecke verwendet.
  • Arithmetische Typen (ganze Zahlen, reelle Gleitkommazahlen und komplexe Zahlen) können auf verschiedene Arten dargestellt werden. Das hängt von der Implementierung ab.
  • Insbesondere können ganzzahlige Typen Füllbits aufweisen . Dies gilt meines Erachtens nicht für Desktop-Computer. Der Standard ließ jedoch die Tür für diese Möglichkeit offen. Füllbits werden für räumliche Zwecke (Parität, Signale, wer weiß) und nicht zum Halten mathematischer Werte verwendet.
  • signed Typen können drei Arten der Darstellung haben: 1-Komplement, 2-Komplement, nur Vorzeichenbit.
  • Die charTypen belegen nur 1 Byte, aber 1 Byte kann eine Anzahl von Bits haben, die sich von 8 unterscheiden (jedoch niemals weniger als 8).
  • Bei einigen Details können wir uns jedoch sicher sein:

    ein. Die charTypen haben keine Füllbits.
    b. Die unsignedInteger-Typen werden genau wie in binärer Form dargestellt.
    c. unsigned charbelegt genau 1 Byte ohne Auffüllbits, und es gibt keine Trap-Darstellung, da alle Bits verwendet werden. Darüber hinaus stellt es einen Wert ohne Mehrdeutigkeit dar, der dem Binärformat für Ganzzahlen folgt.

3. TYP PUNNING vs TYPE REPRÄSENTATION

All diese Beobachtungen zeigen, dass wir viel Unklarheit haben können , wenn wir versuchen, mit Mitgliedern mit unterschiedlichen Typen Typ-Punning durchzuführen . Es ist kein portabler Code und insbesondere könnten wir ein unvorhersehbares Verhalten unseres Programms haben. Der Standard erlaubt jedoch diese Art des Zugriffs . unionunsigned char

Selbst wenn wir uns über die spezifische Art und Weise sicher sind, in der jeder Typ in unserer Implementierung dargestellt wird, könnten wir eine Folge von Bits haben, die in anderen Typen überhaupt nichts bedeuten ( Trap-Darstellung ). In diesem Fall können wir nichts tun.

4. DER SICHERE FALL: Zeichen ohne Vorzeichen

Die einzig sichere Art, Typ Punning zu verwenden, ist mit unsigned charoder gut unsigned charArrays (weil wir wissen, dass Mitglieder von Array-Objekten streng zusammenhängend sind und es keine Auffüllbytes gibt, wenn ihre Größe berechnet wird sizeof()).

  union {
     TYPE data;
     unsigned char type_punning[sizeof(TYPE)];
  } xx;  

Da wir wissen, dass dies unsigned charin strikter binärer Form ohne Auffüllen von Bits dargestellt wird, kann hier der Typ punning verwendet werden, um einen Blick auf die binäre Darstellung des Mitglieds zu werfen data.
Mit diesem Tool kann analysiert werden, wie Werte eines bestimmten Typs in einer bestimmten Implementierung dargestellt werden.

Ich bin nicht in der Lage, eine andere sichere und nützliche Anwendung von Typ Punning unter den Standardspezifikationen zu sehen.

5. EIN KOMMENTAR ZU CASTS ...

Wenn man mit Typen spielen möchte, ist es besser, eigene Transformationsfunktionen zu definieren oder einfach Casts zu verwenden . Wir können uns an dieses einfache Beispiel erinnern:

  union {
     unsigned char x;  
     double t;
  } uu;

  bool result;

  uu.x = 7;
  (uu.t == 7.0)? result = true: result = false;
  // You can bet that result == false

  uu.t = (double)(uu.x);
  (uu.t == 7.0)? result = true: result = false;
  // result == true
pablo1977
quelle
Ich habe kein Zitat aus dem Standard gesehen, das eine Ausnahme für das Typ-Punning über darstellt char, und bin daher sehr skeptisch. Hast du eins? Beachten Sie, dass diese anders als strenges Aliasing könnte gut definiert, die nicht eine Ausnahme für machen charArten. Wir tun gut daran, die beiden nicht zusammenzubringen.
underscore_d
@underscore_d: Es gibt keinen so expliziten Verweis auf Zeichentypen in Typ-Punning. Ich habe mich durch Sammeln von Fakten abgeleitet: Ich kann in Standard C11 lesen, dass (1) Typ-Punning eine gültige Operation in C über Gewerkschaftsmitglieder ist, (2) obwohl Probleme durch die Darstellung von Fallen entstehen können , (3) aber Zeichentypen keine Falle haben Darstellung, (4) jeder Zeichentyp belegt genau 1 Byte. So können Arrays eines Zeichentyps verwendet werden, um die Bytes eines anderen Objekts in einem Gewerkschaftsmitglied zu "lesen". Es gibt jedoch ein undefiniertes Verhalten beim Zugriff auf Mitglieder von Atomgewerkschaften (oder auch Strukturen).
pablo1977
Weißt du, ich glaube, ich habe gerade das Stück übersehen, in dem du gesagt hast, du würdest nur über C sprechen. Es tut uns leid. Anscheinend ist das alles, was ich sehen kann, wenn ich auf einer Mission bin, C ++ zu erforschen, auch wenn es nicht das Thema ist! Ich mag Ihre Argumentation für C, muss aber davon ausgehen, dass es in C ++, das kein Punning zulässt, UB ist, über char(aber nicht über einen Zeiger zu alias). Ich fühle mich wie diese sollten direkt in Beziehung gesetzt werden, aber ich kann nicht eine C ++ Quelle , die sagt : ‚ja, tun , was Sie wollen mit finden charin einem union. aber ich werde das OT auf Ihre Antwort jetzt stoppen :)
underscore_d
4

Es gibt (oder gab es zumindest in C90) zwei Modifikationen, um dieses undefinierte Verhalten zu erzeugen. Das erste war, dass ein Compiler zusätzlichen Code generieren durfte, der verfolgte, was in der Union war, und ein Signal generierte, wenn Sie auf das falsche Mitglied zugegriffen hatten. In der Praxis glaube ich nicht, dass es jemals jemand getan hat (vielleicht CenterLine?). Das andere waren die Optimierungsmöglichkeiten, die sich daraus ergaben und die genutzt werden. Ich habe Compiler verwendet, die einen Schreibvorgang bis zum letztmöglichen Zeitpunkt verschieben würden, da dies möglicherweise nicht erforderlich ist (weil die Variable außerhalb des Gültigkeitsbereichs liegt oder ein nachfolgender Schreibvorgang mit einem anderen Wert erfolgt). Logischerweise würde man erwarten, dass diese Optimierung deaktiviert wird, wenn die Union sichtbar ist, aber nicht in den frühesten Versionen von Microsoft C.

Die Probleme der Typ-Punning sind komplex. Das C-Komitee (Ende der 1980er Jahre) vertrat mehr oder weniger die Position, dass Sie dafür Casts (in C ++, reinterpret_cast) und nicht Gewerkschaften verwenden sollten, obwohl beide Techniken zu dieser Zeit weit verbreitet waren. Seitdem haben einige Compiler (z. B. g ++) den entgegengesetzten Standpunkt vertreten und die Verwendung von Gewerkschaften unterstützt, nicht jedoch die Verwendung von Casts. Und in der Praxis funktioniert beides nicht, wenn nicht sofort ersichtlich ist, dass es zu Typ-Punning kommt. Dies könnte die Motivation hinter g ++ sein. Wenn Sie auf ein Gewerkschaftsmitglied zugreifen, ist sofort ersichtlich, dass es zu Typ-Punning kommen kann. Aber natürlich bei etwas wie:

int f(const int* pi, double* pd)
{
    int results = *pi;
    *pd = 3.14159;
    return results;
}

genannt mit:

union U { int i; double d; };
U u;
u.i = 1;
std::cout << f( &u.i, &u.d );

ist nach den strengen Regeln des Standards vollkommen legal, schlägt jedoch mit g ++ (und wahrscheinlich vielen anderen Compilern) fehl; Beim Kompilieren fgeht der Compiler davon aus pi und pdkann keinen Alias erstellen. Er ordnet das Schreiben in *pdund das Lesen von neu an *pi. (Ich glaube, es war nie die Absicht, dies zu garantieren. Aber der aktuelle Wortlaut des Standards garantiert dies.)

BEARBEITEN:

Da andere Antworten argumentiert haben, dass das Verhalten tatsächlich definiert ist (hauptsächlich basierend auf dem Zitieren einer nicht normativen Notiz, die aus dem Kontext genommen wurde):

Die richtige Antwort ist hier die von pablo1977: Der Standard unternimmt keinen Versuch, das Verhalten zu definieren, wenn es um Typ-Punning geht. Der wahrscheinliche Grund dafür ist, dass es kein tragbares Verhalten gibt, das definiert werden könnte. Dies hindert eine bestimmte Implementierung nicht daran, sie zu definieren. Obwohl ich mich an keine spezifischen Diskussionen zu diesem Thema erinnere, bin ich mir ziemlich sicher, dass die Absicht darin bestand, dass Implementierungen etwas definieren (und die meisten, wenn nicht alle, dies tun).

In Bezug auf die Verwendung einer Union für Typ-Punning: Als das C-Komitee C90 entwickelte (Ende der 1980er Jahre), bestand eindeutig die Absicht, Debugging-Implementierungen zuzulassen, die zusätzliche Überprüfungen durchführten (z. B. die Verwendung von Fettzeigern für die Grenzüberprüfung). Aus den damaligen Diskussionen ging hervor, dass die Absicht bestand, dass eine Debugging-Implementierung Informationen zum letzten in einer Union initialisierten Wert zwischenspeichern und abfangen könnte, wenn Sie versuchen, auf etwas anderes zuzugreifen. Dies ist in §6.7.2.1 / 16 klar festgelegt: "Der Wert von höchstens einem der Mitglieder kann jederzeit in einem Gewerkschaftsobjekt gespeichert werden." Der Zugriff auf einen Wert, der nicht vorhanden ist, ist undefiniert. Es kann dem Zugriff auf eine nicht initialisierte Variable gleichgesetzt werden. (Zu dieser Zeit gab es einige Diskussionen darüber, ob der Zugriff auf ein anderes Mitglied mit demselben Typ legal ist oder nicht. Ich weiß jedoch nicht, wie die endgültige Entschließung lautete. Nach ungefähr 1990 wechselte ich zu C ++.)

In Bezug auf das Zitat aus C89 ist es sehr seltsam, zu sagen, dass das Verhalten implementierungsdefiniert ist: Es in Abschnitt 3 (Begriffe, Definitionen und Symbole) zu finden. Ich muss es zu Hause in meiner Kopie von C90 nachschlagen. Die Tatsache, dass es in späteren Versionen der Standards entfernt wurde, deutet darauf hin, dass seine Anwesenheit vom Ausschuss als Fehler angesehen wurde.

Die Verwendung von Gewerkschaften, die der Standard unterstützt, dient der Simulation der Ableitung. Sie können definieren:

struct NodeBase
{
    enum NodeType type;
};

struct InnerNode
{
    enum NodeType type;
    NodeBase* left;
    NodeBase* right;
};

struct ConstantNode
{
    enum NodeType type;
    double value;
};
//  ...

union Node
{
    struct NodeBase base;
    struct InnerNode inner;
    struct ConstantNode constant;
    //  ...
};

und legal auf base.type zugreifen, obwohl der Knoten über initialisiert wurde inner. (Die Tatsache, dass §6.5.2.3 / 6 mit "Eine besondere Garantie wird gemacht ..." beginnt und dies ausdrücklich zulässt, ist ein sehr starker Hinweis darauf, dass alle anderen Fälle als undefiniertes Verhalten gedacht sind. Und natürlich dort ist die Aussage, dass "undefiniertes Verhalten in dieser Internationalen Norm durch die Worte" undefiniertes Verhalten "oder durch das Weglassen einer expliziten Definition des Verhaltens anderweitig angezeigt wird " in §4 / 2, um zu argumentieren, dass das Verhalten nicht undefiniert ist müssen Sie zeigen, wo es im Standard definiert ist.)

Schließlich in Bezug auf Typ-Punning: Alle (oder zumindest alle, die ich verwendet habe) Implementierungen unterstützen dies in irgendeiner Weise. Mein damaliger Eindruck war, dass die Absicht darin bestand, das Zeiger-Casting so zu gestalten, wie es eine Implementierung unterstützte. Im C ++ - Standard gibt es sogar (nicht normativen) Text, der darauf reinterpret_casthinweist , dass die Ergebnisse von a für jemanden, der mit der zugrunde liegenden Architektur vertraut ist, "nicht überraschend" sind. In der Praxis unterstützen die meisten Implementierungen jedoch die Verwendung von Union für Typ-Punning, vorausgesetzt, der Zugriff erfolgt über ein Gewerkschaftsmitglied. Die meisten Implementierungen (aber nicht g ++) unterstützen auch Zeigerumwandlungen, vorausgesetzt, die Zeigerumwandlung ist für den Compiler deutlich sichtbar (für einige nicht spezifizierte Definitionen der Zeigerumwandlung). Und die "Standardisierung" der zugrunde liegenden Hardware bedeutet, dass Dinge wie:

int
getExponent( double d )
{
    return ((*(uint64_t*)(&d) >> 52) & 0x7FF) + 1023;
}

sind eigentlich ziemlich portabel. (Es funktioniert natürlich nicht auf Mainframes.) Was nicht funktioniert, sind Dinge wie mein erstes Beispiel, bei dem das Aliasing für den Compiler unsichtbar ist. (Ich bin mir ziemlich sicher, dass dies ein Defekt im Standard ist. Ich erinnere mich, dass ich sogar einen DR darüber gesehen habe.)

James Kanze
quelle
3
es war implementierungsdefiniert , nicht undefiniert in C90 - dies illegal zu machen ist ein C ++ - Ismus
Christoph
4
Tatsächlich hat das C-Komitee es illegal gemacht, Zeigerabdrücke für Typ-Punning zu verwenden, indem es eine effektive Typisierung eingeführt hat. Daher ist die Verwendung von Gewerkschaften der C-Weg, dies zu tun
Christoph,
1
@Christoph Es ist immer noch undefiniertes Verhalten in C11, zumindest in der Kopie, die ich habe. §6.7.2.1 / 16 ist darüber ziemlich klar. C ++ ist noch klarer, da das Konzept einer Objektlebensdauer von der Speicherdauer getrennt ist. Selbst in C ist der Zugriff auf ein nicht initialisiertes Objekt (außer als Folge von Bytes) ein undefiniertes Verhalten und die Zuweisung zu einem Element einer Union macht alle anderen "uninitialisiert".
James Kanze
Es tut mir Leid, aber Sie sind falsch, soweit C betroffen ist; Ich habe eine Antwort speziell für Sie geschrieben und die relevanten Zitate
Christoph
@Christoph Das Problem ist, dass Ihre Argumentation weitgehend von einer nicht normativen nicht aus dem Kontext genommenen abhängt. Der wichtige Text befindet sich in §6.7.2.1 / 16. Und C hat das Konzept eines ungültigen Objekts, das beim Zugriff zu undefiniertem Verhalten führt.
James Kanze