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 aus pi
und pd
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.)