Aufruf virtuelle Funktionen in Konstruktoren
Angenommen ich habe zwei C++ - Klassen:
class A
{
public:
A() { fn(); }
virtual void fn() { _n = 1; }
int getn() { return _n; }
protected:
int _n;
};
class B : public A
{
public:
B() : A() {}
virtual void fn() { _n = 2; }
};
Wenn ich den folgenden code schreiben:
int main()
{
B b;
int n = b.getn();
}
Könnte man erwarten, dass n
auf 2 gesetzt ist.
Es stellt sich heraus, dass n
auf 1 gesetzt ist. Warum?
Ich bin zu Fragen, und die Beantwortung meiner eigenen Frage, denn ich möchte die Erklärung für dieses bit von C++ esoterica in Stack-Überlauf. Eine version von diesem Problem heimgesucht hat unser Entwickler-team zweimal, so dass ich vermute, dass diese info von nutzen sein könnten, um jemand da draußen. Bitte schreiben Sie eine Antwort heraus, wenn man erklären kann, ihn in einen anderen/besseren Weg...
Ich Frage mich, warum wurde dies nach unten gestimmt? Als ich zum ersten mal C++ ist dies wirklich verwirrt mich. +1
Was mich wundert, ist das fehlen einer compiler-Warnung. Der compiler ersetzt ein Aufruf an die "definierte Funktion in der Klasse des aktuellen Konstruktor" für das, was in jedem anderen Fall werden die "überschrieben" - Funktion in einer abgeleiteten Klasse. Wenn der compiler sagte, "ersetzt Base::foo() aufrufen, um virtuelle Funktion foo() in Konstruktor" dann der Programmierer würde gewarnt sein, dass der code nicht das tun, was Sie erwartet. Das wäre viel hilfreicher als die silent-substitution, führt zu geheimnisvollen Verhalten, viele debugging, und schließlich eine Reise nach stackoverflow für die Erleuchtung.
Ich Frage mich, warum wurde dies nach unten gestimmt? Als ich zum ersten mal C++ ist dies wirklich verwirrt mich. +1
Was mich wundert, ist das fehlen einer compiler-Warnung. Der compiler ersetzt ein Aufruf an die "definierte Funktion in der Klasse des aktuellen Konstruktor" für das, was in jedem anderen Fall werden die "überschrieben" - Funktion in einer abgeleiteten Klasse. Wenn der compiler sagte, "ersetzt Base::foo() aufrufen, um virtuelle Funktion foo() in Konstruktor" dann der Programmierer würde gewarnt sein, dass der code nicht das tun, was Sie erwartet. Das wäre viel hilfreicher als die silent-substitution, führt zu geheimnisvollen Verhalten, viele debugging, und schließlich eine Reise nach stackoverflow für die Erleuchtung.
InformationsquelleAutor David Coufal | 2009-06-07
Du musst angemeldet sein, um einen Kommentar abzugeben.
Aufrufen von virtuellen Funktionen, die von einem Konstruktor oder Destruktor ist gefährlich und sollte wenn immer möglich vermieden werden. Alle C++ - Implementierungen sollten rufen Sie die version der Funktion, definiert auf der Ebene der Hierarchie in der aktuellen Konstruktor und keine weiteren.
Den C++ FAQ Lite deckt dieses in Abschnitt 23.7 in sehr gutem detail. Ich schlage vor, zu Lesen, dass (und dem rest der FAQ) für follow-up.
Auszug:
BEARBEITEN Korrigiert, Alle (danke litb)
Es ist nicht gefährlich, es ist nur nicht virtuell. In der Tat, wenn Methoden aufgerufen, die Konstruktor genannt wurden, praktisch, würde es gefährlich werden, denn die Methode könnte der Zugriff auf nicht initialisierte Mitglieder.
Warum ist das aufrufen virtueller Funktionen von Destruktor gefährlich? Nicht das Objekt noch vollständig, wenn der Destruktor ausgeführt wird, und nur zerstört, nachdem der Destruktor endet?
-1 "ist gefährlich", Nein, es ist gefährlich in Java, wo downcalls passieren kann; die C++ - Regeln entfernen Sie die Gefahr, durch eine ziemlich teure Mechanismus.
In welcher Weise ist das aufrufen einer virtuellen Funktion aus einem Konstruktor, "gefährliche"? Das ist totaler Quatsch.
InformationsquelleAutor JaredPar
Aufruf einer polymorphen Funktion in einem Konstruktor ist ein Rezept für disaster, die in den meisten OO-Sprachen. Verschiedenen Sprachen durchführen wird anders, wenn diese situation Auftritt.
Das grundlegende problem ist, dass in allen Sprachen der Basis-Typ(en) müssen so konstruiert sein, vor der Abgeleitete Typ. Nun, das problem ist, was es bedeutet, Aufruf einer polymorphen Methode aus dem Konstruktor. Was tun Sie erwarten, dass es sich so Verhalten? Es gibt zwei Ansätze: Aufruf der Methode auf der Basis-Ebene (C++ - Stil) oder rufen Sie die polymorphe Methode auf eine unconstructed Objekt am unteren Ende der Hierarchie (Java-Methode).
In C++ Basisklasse aufbauen wird, seine version der Tabelle virtueller Methoden vor, sich in Ihren eigenen Bau. An dieser Stelle ein Aufruf der virtuellen Methode wird am Ende aufrufen der Basis-version der Methode oder der Herstellung eines rein virtuelle Methode namens im Fall es hat keine Umsetzung auf dieser Ebene der Hierarchie. Nachdem die Basis wurde vollständig aufgebaut, wird der compiler bauen die Abgeleitete Klasse überschreibt die Methode, die Zeiger verweisen auf die Implementierungen in der nächsten Ebene der Hierarchie.
In Java, der compiler erstellen der virtuellen Tabelle entspricht im ersten Schritt der Konstruktion, vor Eintritt in den Basis-Konstruktor oder Abgeleitete Konstruktor. Die Auswirkungen sind unterschiedlich (und um meine Neigungen mehr gefährlich). Wenn die Basisklasse der Konstruktor ruft eine Methode überschrieben, in der abgeleiteten Klasse wird der Anruf auch tatsächlich bearbeitet werden in der abgeleiteten Ebene aufrufen einer Methode auf einem unconstructed-Objekt, woraus sich unerwartete Ergebnisse. Alle Attribute der abgeleiteten Klasse initialisiert werden innerhalb der Konstruktor-block sind noch nicht initialisiert, einschließlich der 'Letzte' Attribute. Elemente, die ein Standard-Wert definiert in der Klasse haben, der Wert.
Wie Sie sehen, der Aufruf einer polymorphen (virtuellen in C++ Terminologie) Methoden ist eine häufige Quelle von Fehlern. In C++, zumindest haben Sie die Garantie, dass es nie eine Methode aufrufen, die auf einem noch unconstructed Objekt...
"Wenn die Basisklasse Konstruktor ruft eine Methode überschrieben, in der abgeleiteten Klasse wird der Anruf auch tatsächlich bearbeitet werden in der abgeleiteten Ebene aufrufen einer Methode auf einem unconstructed-Objekt..." Wie so, wenn base ist bereits initialisiert. Gibt es keine Möglichkeit, es sei denn, Sie explicilty Aufruf "init" initialisieren, bevor die anderen Mitglieder.
Eine Erklärung! +1, superior Antwort imho
Bei mir ist das problem, dass es so viele Einschränkungen in C++ - Klassen, mit seinem unglaublich schwer zu erreichen, jede gute Gestaltung. C++ schreibt vor, dass "Wenn es gefährlich werden könnte, verbieten es", auch wenn seine intuitive wodurch Probleme verursacht werden, wie: "Warum gerade dieses intuitive Verhalten funktioniert nicht" passieren die ganze Zeit.
Was? C++ nicht "verbieten" nichts in diesem Fall. Der Anruf wird einfach behandelt, als ein nicht-virtueller Aufruf der Methode für die Klasse, deren Konstruktor wird gerade ausgeführt. Das ist eine logische Folge der Objekt-Konstruktion auf der Zeitleiste nicht einige drakonische Entscheidung zu stoppen Sie tun dumme Sachen. Die Tatsache, dass es zufällig erfüllt, für den letzteren Zweck ist auch nur ein bonus für mich.
InformationsquelleAutor David Rodríguez - dribeas
Der Grund ist, dass C++ - Objekte werden erstellt, wie ein Zwiebel, von innen heraus. Super-Klassen aufgebaut sind, bevor Sie in abgeleiteten Klassen. Also, bevor ein B gemacht werden kann, muss gemacht werden. Wenn Ein Konstruktor aufgerufen wird, ist es nicht ein B, noch die virtual function table hat immer noch den Eintrag für Eine Kopie von fn().
Das ist das gleiche in den meisten OO-Sprachen: Sie können möglicherweise bauen eine abgeleitete Objekt ohne die Basis, zum Teil bereits gebaut.
andere Sprachen tun, eigentlich tun. Zum Beispiel in Pascal, Arbeitsspeicher ist für das ganze Objekt zuerst, aber dann nur die abgeleitete Konstruktor aufgerufen wird. Ein Konstruktor muss entweder ein expliziter Aufruf von übergeordneten Konstruktor (die nicht die erste Aktion - es muss einfach sein, irgendwo), oder wenn es nicht , es ist, als ob die erste Zeile des Konstruktors gemacht, dass call.
Vielen Dank für die Klarheit und die Vermeidung von details, die nicht gehen direkt auf das Ergebnis
InformationsquelleAutor David Coufal
Den C++ FAQ Lite Deckt das ziemlich gut:
Was "Liebe" schlagen Sie vor? Bedenken Sie, dass Sie können nicht einfach ändern, wie die Dinge derzeit arbeiten in-place, denn das wäre schrecklich brechen Unmengen von vorhandenen code. Also, wie würden Sie es anstellen stattdessen? Nicht nur was neue syntax einführen würde zu ermöglichen (tatsächliche, nicht-devirtualised) virtuelle Aufrufe in Konstruktoren - aber auch, wie Sie würde irgendwie ändern die Modelle der Objekt-Konstruktion/Lebensdauer, so dass diejenigen, die Anrufe hätten eine komplette Objekt des abgeleiteten Typs für die Ausführung. Dies werde interessant sein.
Ich glaube nicht, dass irgendein syntax-änderungen notwendig wären. Vielleicht, wenn Sie ein Objekt erstellen, würde der compiler code hinzufügen, um zu Fuß die vtable und sehen für diesen Fall und patch, was dann? Ich habe nie geschrieben, einen C++ - compiler, und ich bin mir ziemlich sicher, dass mein ersten Kommentar zu geben, einige dieser "Liebe" war naiv und wird nie passieren. 🙂 Eine virtuelle Funktion initialize() ist nicht eine sehr schmerzhafte workaround wie auch immer, du musst nur daran erinnern, rufen Sie nach der Erstellung Ihres Objekts.
Mir fiel nur gerade auf deinen anderen Kommentar, der erklärt, dass die vtable nicht verfügbar im Konstruktor, unterstreicht erneut die Schwierigkeit hier.
Ich patzte beim schreiben über die vtable nicht zur Verfügung stehen, wird im Konstruktor. Es ist zur Verfügung, aber der Konstruktor sieht dann nur die vtable für seine eigene Klasse, da jede abgeleitete Konstruktor aktualisiert die Instanz vptr auf die vtable für die aktuellen abgeleiteten Typ und nicht weiter. So, die aktuelle ctor sieht eine vtable, nur seine eigene überschreibt, also warum kann er nicht rufen mehr-abgeleitete Implementierungen eines virtuellen Funktionen.
InformationsquelleAutor Aaron Maenpaa
Eine Lösung für Ihr problem ist die Verwendung von factory-Methoden das Objekt zu erstellen.
InformationsquelleAutor Tobias
Kennen Sie die crash-Fehler aus dem Windows-explorer?! "Pure virtual function call ..."
Gleiche problem ...
Weil es keine implemetation für die Funktion pureVitualFunction() und die Funktion wird aufgerufen im Konstruktor das Programm wird Abstürzen.
D ' Oh. Die Anrufe gehen über die vtable, aber es wurde noch nicht aktualisiert, und zeigen Sie auf Außerkraftsetzungen, die für die meisten abgeleiteten Klasse: nur der eine gebaut jetzt. Dennoch, das Ergebnis und der Grund für den Absturz bleibt der selbe.
InformationsquelleAutor TimW
Den vtables sind, die vom compiler erstellten.
Ein Klasse Objekt hat einen Zeiger auf die vtable. Wenn es beginnt zu Leben, dass die vtable-Zeiger auf die vtable
der base-Klasse. Am Ende des Konstruktor-code, den der compiler generiert code zu re-Punkt, die vtable-Zeiger
der eigentliche vtable der Klasse. Dadurch wird sichergestellt, dass Konstruktor-code, der Aufrufe virtueller Funktionen ruft die
Basisklasse für Implementierungen von Funktionen, nicht das überschreiben in der Klasse.
C::C
virtuelle Funktionsaufrufe gehen dieC
overrider, nicht zu jeder base-Klasse-version.können Sie erklären, mehr deutlich.
Der dynamische Typ des Objekts ist definiert nach dem ctor aufgerufen hat Basisklasse ctors und bevor es baut für seine Mitglieder. Also der vptr nicht geändert am Ende der ctor.
Ich sage das gleiche, dass vptr nicht geändert am Ende der Konstruktor der Basisklasse, wird es geändert werden am Ende des Konstruktors der abgeleiteten Klasse. Ich hoffe, Sie sagen das gleiche. Seine ein compiler/Umsetzung abhängige Sache. Wenn Sie haben vorgeschlagen, dass vptr ändern sollte. Irgendeinen guten Grund für downvoting?
Der Zeitpunkt der änderung der vptr nicht-Implementierung abhängig. Es ist vorgeschrieben, die durch die Sprache Semantik: der vptr ändert sich, wenn das dynamische Verhalten der Klasse-Instanz-änderungen. Es gibt keine Freiheit hier. Im inneren des Körpers eines ctor
T::T(params)
der dynamische Typ istT
. Der vptr werden widerspiegeln, dass: es wird an vtable für T. sehen Sie das anders?InformationsquelleAutor Yogesh
Den C++ - Standard (ISO/IEC 14882-2014) sagen:
Also, nicht aufrufen
virtual
Funktionen von Konstruktoren oder Destruktoren, die versuche zu nennen, in die das Objekt im Bau oder Zerstörung, Da die Reihenfolge der Bau beginnt von Basis zu abgeleitet und die Reihenfolge der Destruktoren beginnt abgeleitet Basisklasse.So, ist der Versuch zum aufrufen einer abgeleiteten Klasse die Funktion von einer Basis-Klasse im Bau ist gefährlich.Ähnlich, ein Objekt zerstört wird in umgekehrter Reihenfolge von der Konstruktion, also der Versuch zum aufrufen einer Funktion in eine weitere abgeleitete Klasse von einem Destruktor können auf Ressourcen zugreifen, die bereits freigegeben wurden.
InformationsquelleAutor msc
Anderen Antworten bereits erklärt, warum
virtual
Funktionsaufrufe funktionieren nicht wie erwartet, wenn der Aufruf vom Konstruktor. Ich möchte stattdessen vorschlagen, eine andere mögliche Arbeit um für immer polymorph-wie Verhalten von einem Basis-Typ des Konstruktors.Durch hinzufügen einer template-Konstruktor der Basis-Typ, so dass das template-argument ist immer abgeleitet werden die abgeleiteten Typ ist es möglich, sich bewusst sein des abgeleiteten Typs ist es, den konkreten Typ. Von dort aus können Sie anrufen
static
member-Funktionen für die abgeleiteten Typ.Diese Lösung nicht ermöglichen, nicht
static
member-Funktionen aufgerufen werden. Während der Ausführung ist in der Basis-Typ des Konstruktors der abgeleiteten Typ-Konstruktor hat noch nicht einmal Zeit gehabt, zu gehen Sie durch die member-Initialisierungs-Liste. Der abgeleitete Typ Teil von der Instanz erstellt wird, noch nicht begonnen wird initialisiert. Und da nicht-static
member-Funktionen fast sicher, Interaktion mit Daten, Mitgliedern, es wäre ungewöhnlich, um zu wollen rufen Sie den abgeleiteten Typ ist nichtstatic
member-Funktionen aus dem Basis-Typ des Konstruktors.Hier ist eine Beispiel-Implementierung :
Diesem Beispiel sollte drucken
Wenn ein
Derived
gebaut ist, dieBase
Auftragnehmers richtet sich das Verhalten auf die tatsächliche dynamische Typ des Objekts konstruiert.InformationsquelleAutor François Andrieux
Erstens-Objekt erstellt, und dann weisen wir es 's Adresse, auf die Zeiger.Konstruktoren werden aufgerufen, die zum Zeitpunkt der Objekterstellung und verwendet, um initializ der Wert der Daten-Mitglieder. Zeiger auf Objekt kommt in Szenario nach der Objekterstellung. Das ist warum C++ nicht erlaubt Konstruktoren als virtuelle .
.ein weiterer Grund ist, dass Es nichts gibt, wie Zeiger auf Konstruktor , der kann auf virtuellen Konstruktor,weil man von der Eigenschaft der virtuellen Funktion ist, dass es verwendet werden kann, durch den Zeiger nur.
InformationsquelleAutor Priya
Als wurde darauf hingewiesen, daß die Objekte erstellt werden base-down nach Konstruktion. Wenn das Basis-Objekt erstellt wird, das abgeleitete Objekt existiert noch nicht, so eine virtuelle Funktion überschreiben nicht funktionieren kann.
Jedoch, dieses Problem kann mit polymorphen Getter verwenden statischen Polymorphismus anstelle von virtuellen Funktionen, wenn Ihre get-Rückgabe-Konstanten, oder anders ausgedrückt werden kann, die in einer statischen member-Funktion, in Diesem Beispiel verwendet CRTP (https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern).
Mit der Verwendung von statischen Polymorphismus, der Basis-Klasse weiß die Klasse' getter zu nennen, wie die Informationen, die zur compile-Zeit.
Genau:
Base<T>
ist nur eine Hilfsklasse, nicht ein common-interface-Typ, der verwendet werden kann für die Laufzeit-Polymorphismus (f.ex. heterogene Container). Diese sind auch nützlich, nur nicht für die gleichen Aufgaben. Einige Klassen Erben beide von einer Basisklasse, die eine Schnittstelle geben Sie für Laufzeit-Polymorphie und andere, die einen compile-Zeit-template-helper.InformationsquelleAutor stands2reason
Ich bin nicht zu sehen, die Bedeutung der virtuellen Schlüsselwort hier. b ist eine statisch typisierte Variablen und Ihr Typ wird bestimmt durch den compiler zur compile-Zeit. Die Funktion fordert, würde nicht auf die vtable. Wenn b konstruiert, der übergeordneten Klasse der Konstruktor aufgerufen, das ist der Grund, warum der Wert _n auf 1 gesetzt ist.
b
's Konstruktor ruft die basef()
, nicht die abgeleitete überschreiben Sie es. Typ der Variablenb
unerheblich ist, dass."Die Funktion fordert, würde nicht auf die vtable" Das ist nicht wahr. Wenn Sie denken, virtual dispatch ist nur aktiviert, wenn der Zugriff durch eine
B*
oder " B ` &`, irren Sie sich.Abgesehen von der Tatsache, dass er folgt seiner eigenen Logik nach die falsche Schlussfolgerung... die Idee, Die hinter dieser Antwort bekannt statischer Typ ist falsch angewandt. Ein compiler könnte devirtualise
b.getN()
denn er kennt die wahre Art, & direkt-Versand zu der version ausB
. Aber das ist nur eine Zulage erfolgt durch die als-ob Regel. Alles noch handeln müssen, als-ob an die virtuelle Tabelle verwendet wird, &, um den Buchstaben gefolgt. In derA
Konstruktor, das gleiche gilt: auch wenn (wahrscheinlich nicht möglich) es wird inline w/ dieB
ctor, wird der virtuelle Aufruf muss noch handeln als-ob es hat nur die BasisA
vtable zur Verfügung.Können Sie mir ein Beispiel für deine Behauptung, dass virtuelle Versand geschieht ohne den Aufruf über eine Referenz oder ein Zeiger (einschließlich der impliziten
this
)?Sie haben Recht, dass der Aufruf
b.getn()
ist nicht virtuell.b
ist eine statisch typisierte Objekt, und wasgetn()
definiert ist, für seine Art bezeichnet werden. Aber innerhalb von member-Funktionen, einschließlich der Konstruktor alle member-Funktion werden Anrufe durch die implizitethis
Zeiger und sind daher virtuelle Funktion aufruft, wenn es eine polymorphe Klasse. Der Grund und die Begründung für die Auflösung der virtuellenfn()
Aufruf der Basis-Klasse Umsetzung-auch wenn es passiert, während der gesamten Konstruktion eines abgeleiteten Objekts-ist, erklärt in dem anderen Antworten.InformationsquelleAutor user2305329
Während der Objekt-Konstruktor-Aufruf der virtuellen Funktion-Zeiger-Tabelle ist nicht vollständig errichtet. Dadurch wird in der Regel nicht geben Sie das Verhalten, das Sie erwarten. Aufruf einer virtuellen Funktion in dieser situation mag funktionieren, ist aber nicht garantiert und sollten vermieden werden, um tragbar sein, und Folgen Sie den C++ - standard.
der Anruf der base-version, falls verfügbar, oder zum aufrufen von UB, wenn die vfunc ist rein virtuell.
InformationsquelleAutor terry