Nach der Begründung von C89 wollten die Autoren der Norm nicht verlangen, dass Compiler einen Code wie diesen ausgeben:
int x;
int test(double *p)
{
x=5;
*p = 1.0;
return x;
}
sollte erforderlich sein, um den Wert von x
zwischen der Zuweisung und der Rückgabeanweisung, um die Möglichkeit zu berücksichtigen, dass p
könnte darauf hinweisen x
und die Zuordnung zu *p
könnte folglich den Wert von x
. Die Vorstellung, dass ein Compiler das Recht haben sollte, davon auszugehen, dass es kein Aliasing geben wird in Situationen wie der oben genannten war nicht umstritten.
Leider haben die Autoren der C89 ihre Regel so formuliert, dass, wenn man sie wörtlich liest, sogar die folgende Funktion ein nicht definiertes Verhalten hervorrufen würde:
void test(void)
{
struct S {int x;} s;
s.x = 1;
}
weil es einen l-Wert vom Typ int
um auf ein Objekt des Typs struct S
そして int
gehört nicht zu den Typen, die beim Zugriff auf eine struct S
. Da es absurd wäre, jede Verwendung von Nicht-Zeichen-Typ-Mitgliedern von Structs und Unions als unbestimmtes Verhalten zu behandeln, erkennt fast jeder an, dass es zumindest einige Umstände gibt, unter denen ein L-Wert eines Typs verwendet werden kann, um auf ein Objekt eines anderen Typs zuzugreifen. Leider hat es das C-Standardisierungskomitee versäumt, zu definieren, was diese Umstände sind.
Ein Großteil des Problems ist auf den Mängelbericht Nr. 028 zurückzuführen, in dem nach dem Verhalten eines Programms wie diesem gefragt wurde:
int test(int *ip, double *dp)
{
*ip = 1;
*dp = 1.23;
return *ip;
}
int test2(void)
{
union U { int i; double d; } u;
return test(&u.i, &u.d);
}
Defect Report #28 besagt, dass das Programm Undefiniertes Verhalten hervorruft, weil die Aktion des Schreibens eines Unionsmitglieds vom Typ "double" und des Lesens eines vom Typ "int" implementierungsdefiniertes Verhalten hervorruft. Eine solche Argumentation ist unsinnig, bildet aber die Grundlage für die Effective-Type-Regeln, die die Sprache unnötig verkomplizieren und nichts zur Lösung des ursprünglichen Problems beitragen.
Der beste Weg, das ursprüngliche Problem zu lösen, wäre wahrscheinlich eine Behandlung der Fußnote über den Zweck der Regel so zu behandeln, als wäre sie normativ, und die die Regel nicht durchsetzbar zu machen, außer in Fällen, in denen es tatsächlich um widersprüchliche Zugriffe unter Verwendung von Aliasen geht. Gegeben wäre etwas wie:
void inc_int(int *p) { *p = 3; }
int test(void)
{
int *p;
struct S { int x; } s;
s.x = 1;
p = &s.x;
inc_int(p);
return s.x;
}
Es gibt keinen Konflikt innerhalb inc_int
weil alle Zugriffe auf den Speicher, die über *p
werden mit einem l-Wert des Typs int
und es gibt keinen Konflikt in test
denn p
ist sichtbar abgeleitet von einer struct S
und beim nächsten Mal s
verwendet wird, werden alle Zugriffe auf diesen Speicher, die jemals über p
wird bereits geschehen sein.
Wenn der Code leicht geändert würde...
void inc_int(int *p) { *p = 3; }
int test(void)
{
int *p;
struct S { int x; } s;
p = &s.x;
s.x = 1; // !!*!!
*p += 1;
return s.x;
}
Hier gibt es einen Aliasing-Konflikt zwischen p
und den Zugang zu s.x
in der markierten Zeile, weil zu diesem Zeitpunkt der Ausführung ein anderer Verweis existiert die für den Zugriff auf denselben Speicher verwendet werden .
Hätte der Mängelbericht 028 besagt, dass das ursprüngliche Beispiel UB wegen der Überschneidung zwischen der Erstellung und der Verwendung der beiden Zeiger aufruft, wäre die Sache viel klarer gewesen, ohne dass "Effektive Typen" oder eine andere derartige Komplexität hinzugefügt werden müssten.
8 Stimmen
Vielleicht interessieren Sie sich auch für einen Artikel, den ich kürzlich geschrieben habe Was ist die Strict Aliasing Rule und warum ist sie wichtig? . Es deckt eine Menge Material ab, das hier nicht behandelt wird, oder bietet in einigen Bereichen einen moderneren Ansatz.