Aufrufen virtueller Funktionen innerhalb von Konstruktoren

Lesezeit: 12 Minuten

Aufrufen virtueller Funktionen innerhalb von Konstruktoren
David Coufal

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 folgenden Code schreibe:

int main()
{
  B b;
  int n = b.getn();
}

Das könnte man erwarten n ist auf 2 eingestellt.

Es stellt sich heraus, dass n auf 1 gesetzt. Warum?

  • Ich stelle und beantworte meine eigene Frage, weil ich die Erklärung für dieses bisschen C++-Esoterik in Stack Overflow bekommen möchte. Eine Version dieses Problems hat unser Entwicklungsteam zweimal getroffen, daher schätze ich, dass diese Informationen für jemanden da draußen von Nutzen sein könnten. Bitte schreiben Sie eine Antwort, wenn Sie es anders / besser erklären können …

    – David Coufal

    7. Juni 2009 um 15:46 Uhr

  • Ich frage mich, warum das runtergestimmt wurde? Als ich zum ersten Mal C++ lernte, hat mich das wirklich verwirrt. +1

    – Zifre

    7. Juni 2009 um 15:59 Uhr

  • Was mich überrascht, ist das Fehlen einer Compiler-Warnung. Der Compiler ersetzt durch einen Aufruf der „in der Klasse des aktuellen Konstruktors definierten Funktion“ die Funktion, die in jedem anderen Fall die „am häufigsten überschriebene“ Funktion in einer abgeleiteten Klasse wäre. Wenn der Compiler sagt „Ersetzen von Base::foo() für den Aufruf der virtuellen Funktion foo() im Konstruktor“, dann wird der Programmierer gewarnt, dass der Code nicht das tut, was er erwartet. Das wäre viel hilfreicher, als eine stille Substitution vorzunehmen, die zu mysteriösem Verhalten, viel Debugging und schließlich einem Stackoverflow-Trip zur Erleuchtung führen würde.

    – Craig Reynolds

    31. Januar 2019 um 20:02 Uhr

  • @CraigReynolds Nicht unbedingt. Es besteht keine Notwendigkeit für eine spezielle Compilerbehandlung von virtuellen Aufrufen innerhalb von Konstruktoren. Der Basisklassenkonstruktor erstellt die vtable nur für die aktuelle Klasse, sodass der Compiler an diesem Punkt einfach die virtuelle Funktion über diese vtable auf genau die gleiche Weise wie gewöhnlich aufrufen kann. Aber die vtable zeigt noch auf keine Funktion in irgendeiner abgeleiteten Klasse. Die vtable für die abgeleitete Klasse wird vom Konstruktor der abgeleiteten Klasse angepasst, nachdem der Konstruktor der Basisklasse zurückgegeben wurde. So funktioniert die Überschreibung, sobald die abgeleitete Klasse erstellt wurde.

    – Benutzer207421

    5. Februar 2020 um 3:19 Uhr

1647259224 813 Aufrufen virtueller Funktionen innerhalb von Konstruktoren
JaredPar

Das Aufrufen virtueller Funktionen von einem Konstruktor oder Destruktor ist gefährlich und sollte nach Möglichkeit vermieden werden. Alle C++-Implementierungen sollten die Version der Funktion aufrufen, die auf der Ebene der Hierarchie im aktuellen Konstruktor definiert ist, und nicht weiter.

Die C++ FAQ Lite behandelt dies in Abschnitt 23.7 ziemlich ausführlich. Ich schlage vor, das (und den Rest der FAQ) für ein Follow-up zu lesen.

Auszug:

[…] In einem Konstruktor ist der virtuelle Aufrufmechanismus deaktiviert, da das Überschreiben von abgeleiteten Klassen noch nicht stattgefunden hat. Objekte werden von der Basis nach oben konstruiert, „Basis vor Ableitung“.

[…]

Die Zerstörung erfolgt „abgeleitete Klasse vor Basisklasse“, sodass sich virtuelle Funktionen wie in Konstruktoren verhalten: Es werden nur die lokalen Definitionen verwendet – und es werden keine Aufrufe an überschreibende Funktionen vorgenommen, um zu vermeiden, dass der (jetzt zerstörte) abgeleitete Klassenteil des Objekts berührt wird.

BEARBEITEN Am meisten korrigiert (danke litb)

  • Nicht die meisten C++-Implementierungen, aber alle C++-Implementierungen müssen die Version der aktuellen Klasse aufrufen. Wenn einige das nicht tun, dann haben diese einen Fehler :). Ich stimme Ihnen immer noch zu, dass es schlecht ist, eine virtuelle Funktion aus einer Basisklasse aufzurufen – aber die Semantik ist genau definiert.

    – Johannes Schaub – litb

    7. Juni 2009 um 16:19 Uhr

  • Es ist nicht gefährlich, es ist nur nicht virtuell. Tatsächlich wäre es gefährlich, wenn vom Konstruktor aufgerufene Methoden virtuell aufgerufen würden, da die Methode auf nicht initialisierte Member zugreifen könnte.

    – Steven Sudit

    18. Juni 2014 um 19:33 Uhr

  • Warum ist das Aufrufen virtueller Funktionen vom Destruktor gefährlich? Ist das Objekt nicht immer noch vollständig, wenn der Destruktor ausgeführt wird, und wird es erst zerstört, nachdem der Destruktor beendet ist?

    – Siyuan Ren

    11. September 2014 um 12:39 Uhr

  • −1 “ist gefährlich”, nein, es ist gefährlich in Java, wo Downcalls passieren können; die C++-Regeln beseitigen die Gefahr durch einen ziemlich teuren Mechanismus.

    – Prost und hth. – Alf

    14. August 2016 um 10:47 Uhr

  • Inwiefern ist das Aufrufen einer virtuellen Funktion aus einem Konstruktor „gefährlich“? Das ist totaler Unsinn.

    – Leichtigkeitsrennen im Orbit

    5. September 2016 um 20:49 Uhr

1647259225 515 Aufrufen virtueller Funktionen innerhalb von Konstruktoren
David Rodríguez – Dribeas

Der Aufruf einer polymorphen Funktion von einem Konstruktor ist in den meisten OO-Sprachen ein Rezept für eine Katastrophe. Wenn diese Situation auftritt, funktionieren verschiedene Sprachen unterschiedlich.

Das grundlegende Problem besteht darin, dass in allen Sprachen der Basistyp bzw. die Basistypen vor dem abgeleiteten Typ konstruiert werden müssen. Das Problem ist nun, was es bedeutet, eine polymorphe Methode vom Konstruktor aus aufzurufen. Was erwartest du, wie es sich verhalten wird? Es gibt zwei Ansätze: Rufen Sie die Methode auf der Basisebene auf (C++-Stil) oder rufen Sie die polymorphe Methode für ein nicht konstruiertes Objekt am unteren Ende der Hierarchie auf (Java-Weg).

In C++ erstellt die Basisklasse ihre Version der virtuellen Methodentabelle, bevor sie in ihre eigene Konstruktion eintritt. An diesem Punkt führt ein Aufruf der virtuellen Methode dazu, dass die Basisversion der Methode aufgerufen oder eine erzeugt wird rein virtuelle Methode aufgerufen falls es auf dieser Ebene der Hierarchie keine Implementierung gibt. Nachdem die Base vollständig erstellt wurde, beginnt der Compiler mit dem Erstellen der Derived-Klasse und überschreibt die Methodenzeiger, um auf die Implementierungen in der nächsten Ebene der Hierarchie zu zeigen.

class Base {
public:
   Base() { f(); }
   virtual void f() { std::cout << "Base" << std::endl; } 
};
class Derived : public Base
{
public:
   Derived() : Base() {}
   virtual void f() { std::cout << "Derived" << std::endl; }
};
int main() {
   Derived d;
}
// outputs: "Base" as the vtable still points to Base::f() when Base::Base() is run

In Java erstellt der Compiler das virtuelle Tabellenäquivalent im allerersten Konstruktionsschritt, bevor er in den Basiskonstruktor oder den abgeleiteten Konstruktor eintritt. Die Implikationen sind anders (und für meinen Geschmack gefährlicher). Wenn der Konstruktor der Basisklasse eine Methode aufruft, die in der abgeleiteten Klasse überschrieben wird, wird der Aufruf tatsächlich auf der abgeleiteten Ebene behandelt, indem eine Methode für ein nicht konstruiertes Objekt aufgerufen wird, was zu unerwarteten Ergebnissen führt. Alle Attribute der abgeleiteten Klasse, die innerhalb des Konstruktorblocks initialisiert werden, sind noch nicht initialisiert, einschließlich der „final“-Attribute. Elemente mit einem auf Klassenebene definierten Standardwert haben diesen Wert.

public class Base {
   public Base() { polymorphic(); }
   public void polymorphic() { 
      System.out.println( "Base" );
   }
}
public class Derived extends Base
{
   final int x;
   public Derived( int value ) {
      x = value;
      polymorphic();
   }
   public void polymorphic() {
      System.out.println( "Derived: " + x ); 
   }
   public static void main( String args[] ) {
      Derived d = new Derived( 5 );
   }
}
// outputs: Derived 0
//          Derived 5
// ... so much for final attributes never changing :P

Wie Sie sehen, ruft ein polymorphes (virtuell in der C++-Terminologie) Methoden ist eine häufige Fehlerquelle. In C++ haben Sie zumindest die Garantie, dass es niemals eine Methode für ein noch nicht konstruiertes Objekt aufruft …

  • Gut erklärt, warum die Alternative (auch) fehleranfällig ist.

    – DS.

    21. September 2012 um 15:26 Uhr

  • “Wenn der Konstruktor der Basisklasse eine Methode aufruft, die in der abgeleiteten Klasse überschrieben wird, wird der Aufruf tatsächlich auf der abgeleiteten Ebene behandelt, indem eine Methode für ein nicht konstruiertes Objekt aufgerufen wird …” Wieso, wenn die Basis bereits initialisiert ist. Es gibt keine Möglichkeit, es sei denn, Sie rufen ausdrücklich “init” auf, bevor Sie andere Mitglieder initialisieren.

    – Arek Bal

    1. Oktober 2013 um 19:27 Uhr


  • Eine Erklärung! +1, überlegene Antwort imho

    – Unterstrich_d

    3. Juli 2016 um 0:30 Uhr

  • Für mich besteht das Problem darin, dass es in C++-Klassen so viele Einschränkungen gibt, dass es unglaublich schwierig ist, ein gutes Design zu erreichen. C++ diktiert, dass “Wenn es gefährlich sein könnte, verbiete es”, auch wenn es intuitiv Probleme verursacht wie: “Warum dieses intuitive Verhalten nicht funktioniert” ständig vorkommt.

    – VinGarcia

    22. September 2016 um 17:38 Uhr


  • @VinGarcia Was? C++ “verbietet” in diesem Fall nichts. Der Aufruf wird einfach als nicht virtueller Aufruf der Methode für die Klasse behandelt, deren Konstruktor gerade ausgeführt wird. Das ist eine logische Konsequenz aus der Zeitachse der Objektkonstruktion – nicht irgendeine drakonische Entscheidung, um Sie davon abzuhalten, dumme Dinge zu tun. Dass es zufälligerweise auch letzteren Zweck erfüllt, ist für mich nur ein Bonus.

    – Unterstrich_d

    21. Mai 2017 um 13:15 Uhr


Aufrufen virtueller Funktionen innerhalb von Konstruktoren
David Coufal

Der Grund dafür ist, dass C++-Objekte wie Zwiebeln aufgebaut sind, von innen nach außen. Basisklassen werden vor abgeleiteten Klassen erstellt. Bevor also ein B gemacht werden kann, muss ein A gemacht werden. Wenn der Konstruktor von A aufgerufen wird, ist es noch kein B, sodass die virtuelle Funktionstabelle immer noch den Eintrag für die Kopie von fn() von A enthält.

  • C++ verwendet normalerweise nicht den Begriff “Superklasse” – es bevorzugt “Basisklasse”.

    anon

    7. Juni 2009 um 15:48 Uhr

  • Dasselbe gilt für die meisten OO-Sprachen: Sie können unmöglich ein abgeleitetes Objekt erstellen, ohne dass der Basisteil bereits erstellt ist.

    – David Rodríguez – Dribeas

    7. Juni 2009 um 16:12 Uhr

  • @DavidRodríguez-dribeas andere Sprachen tun das tatsächlich. Beispielsweise wird in Pascal zuerst Speicher für das gesamte Objekt zugewiesen, aber dann wird nur der am häufigsten abgeleitete Konstruktor aufgerufen. Ein Konstruktor muss entweder einen expliziten Aufruf an den Konstruktor seines übergeordneten Elements enthalten (was nicht die erste Aktion sein muss – es muss nur irgendwo sein), oder wenn dies nicht der Fall ist, ist es so, als ob die erste Zeile des Konstruktors diesen Aufruf getätigt hätte .

    – MM

    15. Dezember 2015 um 12:52 Uhr

  • Danke für die Klarheit und Vermeidung von Details, die nicht direkt zum Ergebnis führen

    – Benutzer5193682

    1. November 2016 um 19:14 Uhr

  • Wenn der Aufruf immer noch den vptr verwendet (da der vptr auf das aktuelle Level eingestellt ist, wie Sie es auch erwähnt haben) oder nur statisch die Version des aktuellen Levels aufruft.

    – BACKEN ZQ

    14. August 2020 um 15:01 Uhr

Aufrufen virtueller Funktionen innerhalb von Konstruktoren
Aaron Maenpäa

Die C++ FAQ Lite Deckt das ziemlich gut ab:

Während des Aufrufs des Basisklassenkonstruktors ist das Objekt im Wesentlichen noch nicht vom abgeleiteten Typ, und daher wird die Implementierung der virtuellen Funktion des Basistyps aufgerufen und nicht die des abgeleiteten Typs.

Aufrufen virtueller Funktionen innerhalb von Konstruktoren
Tobias

Eine Lösung für Ihr Problem besteht darin, Factory-Methoden zum Erstellen Ihres Objekts zu verwenden.

  • Definieren Sie eine gemeinsame Basisklasse für Ihre Klassenhierarchie, die eine virtuelle Methode afterConstruction() enthält:
class Object
{
public:
  virtual void afterConstruction() {}
  // ...
};
  • Definieren Sie eine Factory-Methode:
template< class C >
C* factoryNew()
{
  C* pObject = new C();
  pObject->afterConstruction();

  return pObject;
}
  • Verwenden Sie es wie folgt:
class MyClass : public Object 
{
public:
  virtual void afterConstruction()
  {
    // do something.
  }
  // ...
};

MyClass* pMyObject = factoryNew();

  • Typ muss für die Vorlagenfunktion angegeben werden MyClass* pMyObject = factoryNew();

    – Raj

    16. August 2019 um 8:32 Uhr

1647259228 275 Aufrufen virtueller Funktionen innerhalb von Konstruktoren
Francois Andrieux

Andere Antworten haben bereits erklärt, warum virtual Funktionsaufrufe funktionieren nicht wie erwartet, wenn sie von einem Konstruktor aufgerufen werden. Ich möchte stattdessen eine andere mögliche Problemumgehung vorschlagen, um ein polymorphes Verhalten vom Konstruktor eines Basistyps zu erhalten.

Durch Hinzufügen eines Vorlagenkonstruktors zum Basistyp, sodass das Vorlagenargument immer als abgeleiteter Typ abgeleitet wird, ist es möglich, den konkreten Typ des abgeleiteten Typs zu erkennen. Von dort aus können Sie anrufen static Memberfunktionen für diesen abgeleiteten Typ.

Diese Lösung erlaubt keine Nicht-static Member-Funktionen aufgerufen werden. Während die Ausführung im Konstruktor des Basistyps erfolgt, hatte der Konstruktor des abgeleiteten Typs noch nicht einmal Zeit, seine Member-Initialisierungsliste durchzugehen. Der Teil des abgeleiteten Typs der zu erstellenden Instanz hat noch nicht damit begonnen, ihn zu initialisieren. Und da nicht-static Elementfunktionen interagieren mit ziemlicher Sicherheit mit Datenelementen, die ungewöhnlich wären will um die Nicht-static Memberfunktionen aus dem Konstruktor des Basistyps.

Hier ist eine Beispielimplementierung:

#include <iostream>
#include <string>

struct Base {
protected:
    template<class T>
    explicit Base(const T*) : class_name(T::Name())
    {
        std::cout << class_name << " created\n";
    }

public:
    Base() : class_name(Name())
    {
        std::cout << class_name << " created\n";
    }


    virtual ~Base() {
        std::cout << class_name << " destroyed\n";
    }

    static std::string Name() {
        return "Base";
    }

private:
    std::string class_name;
};


struct Derived : public Base
{   
    Derived() : Base(this) {} // `this` is used to allow Base::Base<T> to deduce T

    static std::string Name() {
        return "Derived";
    }
};

int main(int argc, const char *argv[]) {

    Derived{};  // Create and destroy a Derived
    Base{};     // Create and destroy a Base

    return 0;
}

Dieses Beispiel sollte gedruckt werden

Derived created
Derived destroyed
Base created
Base destroyed

Wenn ein Derived aufgebaut ist, die Base Das Verhalten des Konstruktors hängt vom tatsächlichen dynamischen Typ des zu erstellenden Objekts ab.

  • Typ muss für die Vorlagenfunktion angegeben werden MyClass* pMyObject = factoryNew();

    – Raj

    16. August 2019 um 8:32 Uhr

1647259228 324 Aufrufen virtueller Funktionen innerhalb von Konstruktoren
steht2grund

Wie bereits erwähnt, werden die Objekte von der Basis nach unten nach der Konstruktion erstellt. Wenn das Basisobjekt erstellt wird, ist das abgeleitete Objekt noch nicht vorhanden, sodass eine virtuelle Funktionsüberschreibung nicht funktionieren kann.

Dies kann jedoch mit polymorphen Gettern gelöst werden, die verwenden statischer Polymorphismus Anstelle von virtuellen Funktionen, wenn Ihre Getter Konstanten zurückgeben oder anderweitig in einer statischen Elementfunktion ausgedrückt werden können, verwendet dieses Beispiel CRTP (https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern).

template<typename DerivedClass>
class Base
{
public:
    inline Base() :
    foo(DerivedClass::getFoo())
    {}

    inline int fooSq() {
        return foo * foo;
    }

    const int foo;
};

class A : public Base<A>
{
public:
    inline static int getFoo() { return 1; }
};

class B : public Base<B>
{
public:
    inline static int getFoo() { return 2; }
};

class C : public Base<C>
{
public:
    inline static int getFoo() { return 3; }
};

int main()
{
    A a;
    B b;
    C c;

    std::cout << a.fooSq() << ", " << b.fooSq() << ", " << c.fooSq() << std::endl;

    return 0;
}

Durch die Verwendung von statischem Polymorphismus weiß die Basisklasse, welcher Getter der Klasse aufgerufen werden soll, da die Informationen zur Kompilierzeit bereitgestellt werden.

  • Ich denke, ich werde es vermeiden. Dies ist keine einzelne Basisklasse mehr. Sie haben tatsächlich viele verschiedene Basisklassen erstellt.

    – Wang

    31. Juli 2018 um 21:12 Uhr

  • @Wang Genau: Base<T> ist nur eine Hilfsklasse, kein allgemeiner Schnittstellentyp, der für Laufzeitpolymorphismus (z. B. heterogene Container) verwendet werden kann. Diese sind auch nützlich, nur nicht für die gleichen Aufgaben. Einige Klassen erben sowohl von einer Basisklasse, die ein Schnittstellentyp für Laufzeitpolymorphismus ist, als auch von einer anderen, die ein Vorlagenhelfer zur Kompilierzeit ist.

    – Neugieriger

    7. November 2018 um 23:53 Uhr

1001450cookie-checkAufrufen virtueller Funktionen innerhalb von Konstruktoren

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy