Objekt mit sich selbst als Referenz konstruieren?

8

Ich habe gerade festgestellt, dass dieses Programm kompiliert und ausgeführt wird (gcc Version 4.4.5 / Ubuntu):

%Vor%

Ich frage mich, warum auf der Erde das überhaupt zustande kommt. Ich gehe davon aus, dass (genau wie in Java) Argumente ausgewertet werden, bevor die Methode / Konstruktor aufgerufen wird, also vermute ich, dass dieser Fall durch einen "Spezialfall" in der Sprachspezifikation abgedeckt sein muss?

Fragen:

  1. Könnte jemand dies erklären (vorzugsweise durch Bezugnahme auf die Spezifikation)?
  2. Was ist der Grund dafür, dies zuzulassen?
  3. Ist es Standard C ++ oder ist es gcc-spezifisch?

EDIT 1: Ich habe gerade gemerkt, dass ich sogar int i = i;

schreiben kann

EDIT 2: Auch mit -Wall und -pedantic wird über Test a(a); nicht geklagt.

EDIT 3: Wenn ich eine Methode hinzufüge

%Vor%

Ich kann sogar Test a(method(a)); ohne Warnungen machen.

    
aioobe 06.12.2010, 16:06
quelle

6 Antworten

8

Der Grund dafür ist, dass die Regeln angeben, dass ein Bezeichnerbereich unmittelbar nach dem Bezeichner beginnt. In dem Fall

%Vor%

die RHS ich ist "nach" der LHS, so dass ich im Umfang bin. Das ist nicht immer schlecht:

%Vor%

, weil eine Variable adressiert werden kann, ohne dass ihr Wert verwendet wird. Im Falle des Copy-Konstruktors des OPs kann kein Fehler leicht angegeben werden, da das Binden eines Verweises auf eine Variable nicht erfordert, dass die Variable initialisiert wird: Dies entspricht dem Entnehmen der Adresse einer Variablen. Ein legitimer Konstruktor könnte sein:

%Vor%

Wenn Sie sehen, dass das Argument nur adressiert ist, wird sein Wert nicht verwendet. In diesem Fall könnte eine Selbstreferenz sinnvoll sein: Das Ende einer Liste ist durch eine Selbstreferenz gegeben. In der Tat, wenn Sie den Typ von "nächsten" zu einer Referenz ändern, gibt es wenig Auswahl, da Sie NULL nicht so leicht wie für einen Zeiger verwenden können.

Wie üblich ist die Frage rückwärts. Die Frage ist nicht, warum eine Initialisierung einer Variablen auf sich selbst verweisen kann, die Frage ist, warum sie sich nicht darauf beziehen kann. [In Felix ist das möglich]. Insbesondere für Typen im Gegensatz zu Variablen ist die fehlende Fähigkeit, eine Referenz weiterzuleiten, extrem gebrochen, da sie verhindert, dass rekursive Typen anders definiert werden als durch Verwendung unvollständiger Typen, was in C ausreichend ist, aber nicht in C ++ aufgrund der Existenz von Vorlagen.

    
Yttrill 06.12.2010, 16:46
quelle
3

Ich habe keine Ahnung, wie das mit der Spezifikation zusammenhängt, aber so sehe ich es:

Wenn Sie Test a(a); ausführen, wird Speicherplatz für a auf dem Stapel zugewiesen. Daher ist der Speicherort von a im Speicher dem Compiler zu Beginn von main bekannt. Wenn der Konstruktor aufgerufen wird (der Speicher wird natürlich vorher zugewiesen), wird der korrekte this -Zeiger an ihn übergeben, weil er bekannt ist.

Wenn du Test *b = new Test(*b); machst, musst du es als zwei Schritte betrachten. Zuerst wird das Objekt zugewiesen und konstruiert, und dann wird der Zeiger auf es zugewiesen b . Der Grund, warum Sie die Nachricht erhalten, ist, dass Sie im Wesentlichen einen nicht initialisierten Zeiger auf den Konstruktor übergeben und ihn mit dem tatsächlichen this -Zeiger des Objekts vergleichen (der schließlich b zugewiesen wird, aber nicht bevor der Konstruktor beendet wird).

    
Matti Virkkunen 06.12.2010 16:15
quelle
3

Der zweite, in dem Sie new verwenden, ist eigentlich einfacher zu verstehen; Was Sie dort aufrufen, ist genau dasselbe wie:

%Vor%

und Sie führen tatsächlich eine ungültige Dereferenzierung durch. Versuchen Sie, einen << &other << zu Ihren cout -Zeilen im Konstruktor hinzuzufügen, und machen Sie das

%Vor%

, um zu sehen, dass Sie den Wert übergeben, den ein Zeiger auf dem Stapel erhalten hat. Wenn nicht explizit initialisiert, ist das nicht definiert. Aber selbst wenn Sie es nicht mit einer Art (un) normalem Standard initialisieren, wird es sich von dem Rückgabewert von new unterscheiden, wie Sie herausgefunden haben.

Stellen Sie sich zunächst einmal vor, dass es sich um eine in-place new handelt. Test a ist eine lokale Variable, nicht ein Zeiger, sie lebt auf dem Stapel und daher ist ihr Speicherort immer gut definiert - dies ist sehr unterschiedlich zu einem Zeiger, Test *b , der, wenn er nicht explizit mit einem gültigen Ort initialisiert wird, hängt .

Wenn Sie Ihre erste Instanziierung schreiben wie:

%Vor%

es wird klarer, was Sie dort aufrufen.

Ich kenne keine Möglichkeit, den Compiler von dieser Art der Selbstinitialisierung aus dem Nichts durch den Kopierkonstruktor abhalten zu lassen (oder gar zu warnen).

    
FrankH. 06.12.2010 16:39
quelle
3

Der erste Fall ist (vielleicht) von 3,8 / 6 abgedeckt:

  

vor der Lebensdauer eines Objekts   begann aber nach der Lagerung die   Das Objekt wird besetzt sein   zugeordnet oder nach der Lebenszeit eines   Objekt ist beendet und vor der   Speicher, den das Objekt belegt hat   wiederverwendet oder freigegeben, jeder lvalue welcher   bezieht sich auf das ursprüngliche Objekt möglicherweise   verwendet, aber nur in begrenztem Umfang. So ein   Lvalue bezieht sich auf zugewiesenen Speicher   (3.7.3.2) und unter Verwendung der Eigenschaften von   der Wert, der nicht von seiner abhängt   Wert ist wohldefiniert.

Da Sie nur die Adresse von a (und other , die an a gebunden ist) vor dem Anfang ihrer Lebensdauer verwenden, denke ich, dass Sie gut sind: Lesen Sie den Rest dieses Absatzes für die detaillierten Regeln.

Beachten Sie jedoch, dass 8.3.2 / 4 besagt: "Eine Referenz soll initialisiert werden, um auf ein gültiges Objekt oder eine gültige Funktion zu verweisen." Es gibt eine Frage (als ein Fehlerbericht über den Standard), was "gültig" in diesem Kontext bedeutet, also können Sie möglicherweise den Parameter other nicht an den unstrukturierten (und daher "ungültigen"?)% Co_de% binden.

Also, ich bin unsicher, was der Standard tatsächlich hier sagt - ich kann einen Lvalue verwenden, aber nicht an eine Referenz binden, in diesem Fall ist a nicht gut, Das Übergeben eines Zeigers an a wäre in Ordnung, solange es nur in den von 3.8 / 5 zugelassenen Möglichkeiten verwendet wird.

Im Falle von a verwenden Sie den Wert vor der Initialisierung (weil Sie ihn dereferenzieren, und auch, wenn Sie so weit gekommen sind, wäre b der Wert von &other ). Das ist eindeutig nicht gut.

Wie immer in C ++, kompiliert es, weil es keine Verletzung von Spracheinschränkungen ist und der Standard nicht explizit eine Diagnose erfordert. Stellen Sie sich die Verdrehungen vor, die die Spezifikation durchlaufen müsste, um eine Diagnose zu berechtigen, wenn ein Objekt in seiner eigenen Initialisierung ungültig verwendet wird, und stellen Sie sich die Datenflussanalyse vor, die ein Compiler möglicherweise durchführen muss, um komplexe Fälle zu identifizieren möglich zur Kompilierzeit, wenn der Zeiger durch eine extern definierte Funktion geschmuggelt wird). Es ist einfacher, es als undefiniertes Verhalten zu belassen, es sei denn jemand hat wirklich gute Vorschläge für eine neue Spezifikationssprache ;-)

    
Steve Jessop 06.12.2010 19:28
quelle
2

Wenn Sie Ihre Warnstufe erhöhen, warnt Sie Ihr Compiler wahrscheinlich davor, nicht initialisiertes Material zu verwenden. UB benötigt keine Diagnose, viele Dinge, die "offensichtlich" falsch sind, können kompiliert werden.

    
etarion 06.12.2010 16:13
quelle
0

Ich kenne den Spezifikationsverweis nicht, aber ich weiß, dass der Zugriff auf einen nicht initialisierten Zeiger immer zu undefiniertem Verhalten führt.

Wenn ich Ihren Code in Visual C ++ kompiliere, bekomme ich:

  

test.cpp (20): Warnung C4700:   nicht initialisierte lokale Variable 'b' verwendet

    
Steve Townsend 06.12.2010 16:16
quelle

Tags und Links