8

Ich bin verwirrt über die C++ - Aliasing-Regel und ihre möglichen Auswirkungen. Betrachten Sie den folgenden Code ein:C/C++ striktes Aliasing, Object Lifetime und moderne Compiler

Wenn ein:

int main() { 
    int32_t a = 5; 
    float* f = (float*)(&a); 
    *f = 1.0f; 

    int32_t b = a; // Probably not well-defined? 
    float g = *f; // What about this? 
} 

auf die Spezifikationen ++ C Sehen, Abschnitt 3.10.10, technisch keine der gegebenen Code scheint die "Aliasing-Regeln" dort gegeben zu verletzen Programm versucht, den gespeicherten Wert eines Objekts durch einen L-Wert von anderen als einer der folgenden Typen zuzugreifen das Verhalten undefiniert:
... eine Liste qualifizierter Accessor-Typen ...

  • *f = 1.0f; bricht nicht die Regeln, weil es keinen Zugriff auf einen gespeicherten Wert gibt, d.h. ich schreibe gerade in den Speicher über einen Zeiger. Ich lese nicht aus dem Gedächtnis oder versuche hier einen Wert zu interpretieren.
  • Die Zeile int32_t b = a; verstößt nicht gegen die Regeln, weil ich auf den ursprünglichen Typ zugreife.
  • Die Linie float g = *f; bricht nicht die Regeln aus dem gleichen Grund.

In another thread, Mitglied CortAmmon macht tatsächlich den gleichen Punkt in einer Reaktion, und fügte hinzu, dass eine mögliche undefinierte Verhalten lebendig Objekte durch schreibt entstehen, wie in *f = 1.0f;, für die von der Norm berücksichtigt würden Definition des Begriffs " Objektlebensdauer "(was für POD-Typen trivial zu sein scheint).

JEDOCH: Es gibt viel von Beweisen im Internet, dass über Code wird UB auf modernen Compilern produzieren. Siehe zum Beispiel here und here.
Die Argumentation in den meisten Fällen ist, dass der Compiler frei ist, &a und f als nicht aliasing gegenseitig zu betrachten und daher frei, Befehle neu zu planen.

Die große Frage ist jetzt, ob solch ein Compilerverhalten tatsächlich eine "Überinterpretation" des Standards wäre.
Das einzige Mal, dass der Standard speziell über "Aliasing" spricht, ist in einer Fußnote zu 3.10.10, wo klargestellt wird, dass dies die Regeln sind, die Aliasing regeln sollen.
Wie ich bereits erwähnt habe, sehe ich keinen der oben genannten Code gegen den Standard, aber es würde von einer großen Anzahl von Menschen (und möglicherweise Compiler Menschen) für illegal gehalten werden.

Ich würde wirklich einige Abklärung hier wirklich schätzen.

Kleines Update:
Als Mitglied BenVoigt Recht darauf hingewiesen, int32_t mit float auf einigen Plattformen nicht ausrichten kann, so dass der gegebene Code in Verletzung der „Lagerung von ausreichender Ausrichtung und Größe“ Regel sein kann. Ich würde gerne sagen, dass int32_t bewusst auf float auf den meisten Plattformen ausgerichtet wurde und dass die Annahme für diese Frage ist, dass die Typen tatsächlich ausrichten.

Kleines Update # 2:
Da mehrere Mitglieder darauf hingewiesen haben, int32_t b = a; die Linie ist wahrscheinlich eine Verletzung der Norm, wenn auch nicht mit absoluter Sicherheit. Ich stimme diesem Standpunkt zu und, ohne jeden Aspekt der Frage zu ändern, bitte Leser, diese Zeile von meiner obigen Aussage auszuschließen, dass keiner der Codes gegen den Standard verstößt.

+2

Schreiben ist eine Form des Zugriffs so viel wie das Lesen ist. –

+2

Wenn das wahr wäre, würde der Standard eher "Zugriff auf die Erinnerung eines Objekts durch ..." sagen. Aber was es sagt ist "Zugriff auf den gespeicherten Wert", was nicht der Code ist. – rsp1984

+1

Neben anderen Problemen mit diesem Code hatten Sie nie ein Objekt vom Typ 'float', weil Sie nie" Speicher von ausreichender Größe und korrekter Ausrichtung erhalten haben ". 'a' ist so groß und ausgerichtet für' int', nicht 'float', und einige Plattformen werden Sie wirklich wissen lassen (um es freundlich zu sagen). –

Antwort

5

Sie liegen falsch in Ihrem dritten Punkt (und vielleicht auch der erste).

Sie sagen "Die Zeile float g = *f; bricht nicht die Regeln aus dem gleichen Grund.", Wo "nur der gleiche Grund" (ein wenig vage) auf "Zugriff durch seinen ursprünglichen Typ" zu beziehen scheint. Aber das machst du nicht. Sie greifen auf einen int32_t (mit dem Namen a) über einen Lvalue des Typs float (erhalten aus dem Ausdruck *f). Sie verstoßen also gegen den Standard.

Ich glaube auch (aber weniger sicher in diesem), dass das Speichern eines Wertes ein Zugriff auf (das) gespeicherte Wert ist, so dass sogar *f = 1.0f; gegen die Regeln verstößt.

+0

Der Standard besagt, dass die Lebensdauer eines Objekts endet, wenn der Speicher freigegeben oder wiederverwendet wird. Das mache ich mit '* f = 1.0f;'. Daher ist das betrachtete Objekt vom Typ float und die Zeile float g = * f; kann als legal angesehen werden. – rsp1984

+1

Aber das "Objekt" ist in diesem Fall "a", nicht die "5", die zufälligerweise "in" "a" ist. Also ist es immer noch UB. –

+1

Aber 'a' ist tot nach' * f = 1.0f; 'nicht wahr? – rsp1984

2

Ich denke, diese Aussage falsch ist:

Die Linie int32_t b = a; verstößt nicht gegen die Regeln, weil ich über seinen ursprünglichen Typ zugreife.

Das Objekt, das &a an der Stelle gespeichert ist, ist jetzt ein Schwimmer, so dass Sie versuchen, den gespeicherten Wert eines Schwimmers durch einen L-Wert vom falschen Typ zuzugreifen.

1

Es gibt einige signifikante Mehrdeutigkeiten in der Spezifikation der Lebensdauer und des Zugriffs auf Objekte, aber hier sind einige Probleme mit dem Code, die ich in der Spezifikation gelesen habe.

float* f = (float*)(&a); 

Dies führt eine reinterpret_cast und solange float erfordert keine strengere Ausrichtung als int32_t dann können Sie den resultierenden Wert zurückgeworfen auf eine int32_t* und Sie werden den ursprünglichen Zeiger erhalten. Die Verwendung des Ergebnisses ist in keinem Fall anders definiert.

*f = 1.0f; 

Angenommen *f Aliase mit a (und dass die Lagerung für ein int32_t hat die geeignete Ausrichtung und Größe für ein float), dann die obige Zeile endet die Lebensdauer des int32_t Objekts und legt ein float Objekt in seinem Platz:

Die Lebensdauer eines Objekts vom Typ T beginnt, wenn: Speicher mit der richtigen Ausrichtung und Größe für Typ T erhalten wird, und wenn das Objekt nicht triviale Initialisierung hat, ist seine Initialisierung abgeschlossen.

Die Lebensdauer eines Objekts vom Typ T endet, wenn: [...] der Speicher, den das Objekt belegt, wiederverwendet oder freigegeben wird.

— 3.8 Objektlebensdauer [basic.Leben]/1

Wir Wiederverwendung der Lagerung, aber wenn int32_t die gleiche Größe und Ausrichtung Anforderungen hat dann scheint es wie ein float immer an der gleichen Stelle existiert (da die Lagerung ‚‘ erhalten wurde). Vielleicht können wir diese Mehrdeutigkeit vermeiden, indem wir diese Zeile auf new (f) float {1.0f}; ändern, so dass wir wissen, dass das float Objekt eine Lebensdauer hat, die bei oder vor dem Abschluss der Initialisierung begann.

Darüber hinaus bedeutet "Zugriff" nicht unbedingt nur "lesen". Es kann sowohl Lese- als auch Schreibvorgänge bedeuten. So könnte der von *f = 1.0f; ausgeführte Schreibvorgang als "Zugriff auf den gespeicherten Wert" betrachtet werden, indem über ihn geschrieben wird, in welchem ​​Fall dies auch eine Aliasing-Verletzung ist.

unter der Annahme, also jetzt, dass ein Schwimmer Objekt vorhanden ist und die int32_t Lebensdauer des Objekts beendet:

int32_t b = a; 

Dieser Code den gespeicherten Wert eines mit Typ float Objekt zugreift int32_t, ein glvalue durch und ist eindeutig eine Aliasing-Verletzung . Das Programm hat ein undefiniertes Verhalten unter 3.10/10.

float g = *f; 

Unter der Annahme, dass int32_t die richtige Ausrichtung und Größenanforderungen hat, und dass der Zeiger f in eine Weise erhalten wurde, die seine Verwendung ermöglicht auch definiert werden, dann sollte dies rechtlich Zugriff auf das float Objekt, das mit dem Initialisieren 1.0f.

+0

Danke, die Meinung zu dieser Angelegenheit wirklich hilfreich zu bekommen. Ich stimme zu, dass "int32_t b = a;" wahrscheinlich eine Alias-Verletzung ist, obwohl ich in der Frage vielleicht etwas anderes gesagt habe. Was die verbleibenden Unklarheiten angeht, gibt es irgendeine Möglichkeit, mit den Leuten in Kontakt zu treten, die den Standard geschrieben haben und um Klärung bitten? – rsp1984

+0

@RafaelSpring http://isocpp.org/std/submit-a-library-issue – bames53

0

Ich habe auf die harte Tour gelernt, dass 6.5.7 aus dem C99-Standard unter Angabe nicht hilfreich ist, ohne auch auf 6.5.6 zu suchen. Die entsprechenden Zitate finden Sie unter this answer.

6.5.6 macht deutlich, dass sich der Typ eines Objekts unter Umständen während seiner Lebensdauer mehrfach ändern kann. Es kann den Typ des Wertes annehmen, der zuletzt geschrieben wurde. Das ist wirklich nützlich.

Wir müssen zwischen "deklariertem Typ" und "effektivem Typ" unterscheiden. Eine lokale Variable oder statisch global hat einen deklarierten Typ. Du bist fest mit diesem Typ, denke ich, für die Lebenszeit dieses Objekts. Sie können aus dem Objekt mit einem char * lesen, aber der "effektive Typ" ändert sich leider nicht.

Der von malloc zurückgegebene Speicher hat jedoch "keinen deklarierten Typ". Dies bleibt so lange wahr, bis es free d. Es wird niemals einen deklarierten Typ haben, aber der effektive Typ kann sich gemäß 6.5.6 ändern, wobei immer der Typ des letzten Schreibvorgangs übernommen wird.

Also, das ist legal:

int main() { 
    void * vp = malloc(sizeof(int)+sizeof(float)); // it's big enough, 
        // and malloc will look after alignment for us. 
    int32_t *ap = vp; 
    *ap = 5;  // make int32_t the 'effective type' 
    float* f = vp; 
    *f = 1.0f; // this (legally) changes the effective type. 

    // int32_t b = *ap; // Not defined, because the 
          // effective type is wrong 
    float g = *f; // OK, because the effective type is (currently) correct. 
} 

Also, im Grunde auf einen malloc -ed Raum Schreiben eine gültige Weise ist seine Art zu ändern. Aber ich schätze, das gibt uns nicht die Möglichkeit, das Vorhandene durch die "Linse" eines neuen Typs zu betrachten, was interessant sein könnte; es ist unmöglich, es sei denn, ich denke, wir verwenden die verschiedenen char* Ausnahmen, um Daten vom "falschen" Typ zu sehen.