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:
- Warum ist es schlecht, Gewerkschaften für Typ Punning zu verwenden?
- 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.
quelle
Antworten:
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:
C11 Abschnitt 6.5.2.3 §3:
mit folgender Fußnote 95:
Dies sollte völlig klar sein.
James ist verwirrt, weil C11 Abschnitt 6.7.2.1 §16 lautet
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:
In C99 wurde dies früher gelesen
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.
quelle
memcpy
gelangt sind ( durch Modifikation überchar *
, durch ein anderes Gewerkschaftsmitglied, ...). Sie werden mich nicht anders überzeugen können. Wenn Sie es sich nicht anders überlegen, ist es wahrscheinlich sinnlos, fortzufahren ...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 :
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:
in C11 wäre das eine Fußnote
95
.Obwohl im
std-discussion
Mail-Gruppenthema Typ Punning über eine Union das Argument vorgebracht wird, ist dies unterbestimmt, was vernünftig erscheint, daDR 283
kein neuer normativer Wortlaut hinzugefügt wurde, sondern nur eine Fußnote: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:
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:
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 .
quelle
std::variant
als Variantentyp bekommenstd::memcpy
nur gültig ist, wenn die Typen TriviallyCopyable sinduint16_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 eroutptr
bei jedem Durchlauf durch die Schleife neu laden und neu speichern muss, wenn Code verwendet wirdmemcpy
. Wenn man sich darauf verlassen könnte, dass der Compiler die Besetzunguint16_t*
als Zeichen dafür behandelt, dass die Funktion auf Dinge vom Typ zugreifen könnteuint16_t
oderuint32_t
CodeEs ist legal in C99:
Aus dem Standard: 6.5.2.3 Struktur und Gewerkschaftsmitglieder
quelle
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:
struct
oder einunion
Objekt angewendet wird und welcher Wert erhalten wird.Genau dort erscheint die Fußnote 95 . Diese Fußnote sagt:
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
unions
darin, 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,union
stattdessen astruct
zum Speichern von Speicher verwendet werden.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:
union
Mitglieder haben dieselbe Überschriftenadresse im Speicher und dieselbe Adresse wie dasunion
Objekt selbst.struct
Mitglieder haben eine zunehmende relative Adresse, indem sie in genau derselben Speicheradresse beginnen wie dasstruct
Objekt 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.signed
Typen können drei Arten der Darstellung haben: 1-Komplement, 2-Komplement, nur Vorzeichenbit.char
Typen 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
char
Typen haben keine Füllbits.b. Die
unsigned
Integer-Typen werden genau wie in binärer Form dargestellt.c.
unsigned char
belegt 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 .
union
unsigned 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 char
oder gutunsigned char
Arrays (weil wir wissen, dass Mitglieder von Array-Objekten streng zusammenhängend sind und es keine Auffüllbytes gibt, wenn ihre Größe berechnet wirdsizeof()
).union { TYPE data; unsigned char type_punning[sizeof(TYPE)]; } xx;
Da wir wissen, dass dies
unsigned char
in 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 werfendata
.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
quelle
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 machenchar
Arten. Wir tun gut daran, die beiden nicht zusammenzubringen.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 findenchar
in einemunion
. aber ich werde das OT auf Ihre Antwort jetzt stoppen :)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
f
geht der Compiler davon auspi
undpd
kann keinen Alias erstellen. Er ordnet das Schreiben in*pd
und 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_cast
hinweist , 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.)
quelle