2016-05-24 8 views
2

Bedenken Sie:Ist Zugriff auf ein Mitglied einer Struktur in seinem eigenen initializer undefiniertes Verhalten

struct A { void do_something() {} }; 

struct B { 
    A& a; 
    B(A& a) : a{a} { a.do_something(); } 
}; 

struct C { 
    A a; B b; 
    C(A& a) : b{a} {} 
}; 

int main() { 
    C c{ c.a }; 
} 

Es scheint möglich, dass diese könnte zur Arbeit gemacht werden, denn:

  • noch vor c ist initialisiert, wir kennen sein Speicherlayout und die Adresse c.a
  • wir nicht tatsächlich verwendenc.a bis es initialisiert ist.

Zusätzlich habe ich unter ein paar verschiedenen Compilern keine Warnung erhalten.

Allerdings hatte ich ein extrem merkwürdiges Verhalten (etwas später), das nur darauf zurückzuführen war, etwas Undefiniertes zu tun, und das ging nur weg, wenn ich mein Programm neu organisierte, um dieses Muster zu vermeiden.

Danke.

+1

Zugriff 'c.a' * außerhalb 'C' Konstruktor * und vor 'c' Lebzeiten ein Problem begonnen hat. Es ist ein bisschen wie 'int x = x;'. –

+0

Aber mein Programm funktioniert perfekt! – STU

+0

@STU, undefiniertes Verhalten bedeutet nicht, dass der Code nicht funktionieren würde, (und Ihr derzeitiges Snippet funktioniert gut und sollte für jeden vernünftigen Compiler gut funktionieren). Aber UB bedeutet auch, dass der Compiler Code erzeugen darf, der das wird formatiere deinen PC neu; ;-) – WhiZTiM

Antwort

2

Ihr Programm hat ein undefiniertes Verhalten, weil Sie auf das Objekt eines Objekts außerhalb des Objekts zugreifen und bevor die Lebensdauer des Objekts beginnt.

$12.7: 1: Für ein Objekt mit einem nicht-trivialen Konstruktor zu beziehenden nicht statischem Elemente oder Basisklasse des Objektes vor der Konstruktor beginnt Ausführungsergebnisse in undefiniertem Verhalten ...

wie, warum es kompiliert fein: Ein Name nach dem Punkt der Erklärung verwendet werden kann ... unter Angabe der C++ Standard-Entwurf ... (Hervorhebung von mir):

$3.3.2 : 1: der Punkt declaratio n für einen Namen ist sofort nach seiner vollständigen declarator (Ziffer [dcl.decl]) und vor seinem initializer (falls vorhanden) ...

Und initalizer definiert die Syntax haben (hier teilweise wiedergegeben):

initializer ...

initializer: 
    brace-or-equal-initializer 
    (expression-list) 

brace-or-equal-initializer: 
    = initializer-clause 
    braced-init-list 

initializer-clause: 
    assignment-expression 
    braced-init-list 
. . . 

Die oben ist der gleiche Grund, warum diese kompilieren:

int k(k); 
int m{m}; 
int b = b; 
+0

Ich bin nicht sicher, ob die Grammatik (die nur beschreibt, wie C++ - Quelle geparst wird) für die Laufzeit überhaupt relevant ist. Dieser Abschnitt sagt uns nur, an welcher Stelle im Quelltext ein bestimmtes Token eine bestimmte Bedeutung hat, aber das ist ein Punkt im Text, nicht ein Zeitpunkt. – MSalters

2

Neben meiner vorherigen Antwort,


Ihr Code ist ein schlauer ein ... Denn trotz der soll UB mit einem Objekt vor seiner Initialisierung verwendet, ist das Verhalten Ihres Codes offenbar gut definiert ..

Wie? Bei der Konstruktion von c wird die folgende Sequenz von Ereignissen geschehen:

  1. Sie rufen C ‚s Konstruktor C(A& a) : b{a} {} die A eine Referenz auf ein Objekt vom Typ nimmt. (Eine Referenz ist wie eine Adresse, und wie Sie richtig erwähnten, ist die Adresse c.a zur Kompilierungszeit bekannt). Ihr Anruf ist: C c{ c.a }; und der Compiler mit, dass in Ordnung, da c.a ist ein leicht zugänglicher Name

  2. Aufgrund der Reihenfolge der Deklaration von C ‚s Mitgliedern ...

    struct C { 
        A a; B b; 
        C(A& a) : b{a} {} 
    }; 
    

    das Objekt a initialisiert vor b.

  3. So a vor seiner Verwendung lebendig wird in der Elementinitialisierung ... b{a}


Aber auch hier kann man durch optimizers geraucht werden ...

+0

Das war meine Argumentation, aber wie ich sagte, endete es damit, dass ich 20 Zeilen später seltsame Bugs bekam, bis ich es reparierte. – STU

0

C++ a hat ziemlich gut definierte Reihenfolge der Konstruktion. Erste (nicht virtuelle) Basisklassen werden initialisiert. Mitglieder werden als nächstes in der Reihenfolge ihrer Deklaration in der Klasse initialisiert (und ignoriert die Reihenfolge der Initialisierungsliste), und erst nachdem das letzte Mitglied initialisiert wurde, wird der Körper des Konstruktors eingegeben.

In diesem Fall bedeutet dies, dass die Ctors in der Reihenfolge A::A, B::B, C::C aufgerufen werden.

Sie würde haben ein Problem, wenn C erklärt wurde als

struct C { 
    B b; A a; 
    C(A& a) : b{a} {} 
}; 
+0

Nun, es gibt immer noch das Problem von [12.7p1], das sagt * Für ein Objekt mit einem nicht-trivialen Konstruktor führt die Bezugnahme auf ein nicht-statisches Element oder eine Basisklasse des Objekts, bevor der Konstruktor beginnt, zu undefiniertem Verhalten. [...] *. Dies kann jedoch in Zukunft angepasst werden (siehe meinen Kommentar zu der Frage). – bogdan

+0

@bogdan: Ich habe es bemerkt, habe das CWG-Problem gelesen, habe dich aufgezogen und dann die Antwort geschrieben. Wie Sie aus der CWG-Frage schließen können, erlauben alle "vernünftigen Interpretationen" die Bildung einer Referenz. – MSalters

+0

Die "sinnvolle Auslegung" in der CWG-Ausgabe bezieht sich auf die Auslegung des Wortes * access * in [3.8], und das wurde zwischenzeitlich im Standardwortlaut geklärt, daher sind wir uns einig. Ich habe das kommentiert, weil ich deine Antwort gelesen habe, um zu sagen "Es geht dir gut", und ich denke nicht, dass das eine sichere Annahme ist, solange [12.7p1] den Code eindeutig in UB (in 'C c {ca}) ergibt. ; ', der Ausdruck 'ca' wird ausgewertet, bevor der Konstruktor von 'c' gestartet wird. – bogdan

Verwandte Themen