web-dev-qa-db-de.com

Virtuelle Funktionen innerhalb von Konstruktoren aufrufen

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();
}

Man könnte erwarten, dass n auf 2 gesetzt ist.

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

200
David Coufal

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

Der C++ FAQ Lite behandelt dies in Abschnitt 23.7 ziemlich genau. Ich empfehle, das (und den Rest der FAQ) für ein Follow-up zu lesen.

Auszug:

[...] In einem Konstruktor ist der Mechanismus für virtuelle Aufrufe deaktiviert, da das Überschreiben von abgeleiteten Klassen noch nicht erfolgt ist. Objekte werden von der Basis aus aufgebaut, "Basis vor abgeleiteten".

[...]

Die Zerstörung erfolgt "abgeleitete Klasse vor Basisklasse", daher verhalten sich virtuelle Funktionen wie in Konstruktoren: Nur die lokalen Definitionen werden verwendet - und es werden keine Aufrufe zum Überschreiben von Funktionen gemacht, um zu vermeiden, dass der (jetzt zerstörte) abgeleitete Klassenteil des Objekts berührt wird.

EDIT Am meisten für alle korrigiert (danke litb)

186
JaredPar

Das Aufrufen einer polymorphen Funktion aus einem Konstruktor ist in den meisten OO - Sprachen ein Notfallrezept. Verschiedene Sprachen werden in dieser Situation unterschiedlich funktionieren.

Das grundlegende Problem ist, dass in allen Sprachen die Basistypen vor dem Abgeleiteten Typ erstellt werden müssen. Das Problem ist nun, was es bedeutet, eine polymorphe Methode vom Konstruktor aus aufzurufen. Wie erwarten Sie sich so verhalten? Es gibt zwei Ansätze: Rufen Sie die Methode auf Basisebene (C++ - Stil) auf oder rufen Sie die polymorphe Methode für ein nichtkonstruiertes Objekt am unteren Ende der Hierarchie (Java-Methode) auf.

In C++ erstellt die Base-Klasse ihre Version der virtuellen Methodentabelle, bevor sie in ihre eigene Konstruktion eingeht. An diesem Punkt wird ein Aufruf der virtuellen Methode am Ende die Basisversion der Methode aufrufen oder eine pure virtuelle Methode namens erzeugen, falls auf dieser Hierarchieebene keine Implementierung erfolgt. Nachdem die Basis vollständig erstellt wurde, beginnt der Compiler mit der Erstellung der abgeleiteten Klasse und überschreibt die Methodenzeiger, um auf die Implementierungen in der nächsten Ebene der Hierarchie zu verweisen.

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 ersten Schritt der Konstruktion, bevor er in den Basiskonstruktor oder den abgeleiteten Konstruktor gelangt. Die Auswirkungen sind unterschiedlich (und nach meinem Geschmack gefährlicher). Wenn der Basisklassenkonstruktor eine Methode aufruft, die in der abgeleiteten Klasse überschrieben wird, wird der Aufruf tatsächlich auf der abgeleiteten Ebene behandelt, wobei eine Methode für ein nichtkonstruiertes 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 Attribute 'final'. Elemente, deren Standardwert auf Klassenebene definiert ist, 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, ist das Aufrufen einer polymorphen Methode ( virtual in C++ - Terminologie) eine häufige Fehlerquelle. In C++ haben Sie zumindest die Garantie, dass niemals eine Methode für ein noch nicht erstelltes Objekt aufgerufen wird ...

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

54
David Coufal

Das C++ FAQ Lite deckt dies ziemlich gut ab:

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

22
Aaron Maenpaa

Eine Lösung für Ihr Problem ist die Verwendung von Factory-Methoden zur Erstellung Ihres Objekts.

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

 return pObject; 
} 
  • Verwenden Sie es so:
 Klasse MyClass: public Objekt 
 {
 public: 
 virtuelles void afterConstruction () 
 {
 // etwas tun.
 } 
 // ...
}; 

 MyClass * pMyObject = factoryNew (); 

13
Tobias

Der C++ - Standard (ISO/IEC 14882-2014) sagen:

Elementfunktionen einschließlich virtueller Funktionen (10.3) können als .__ aufgerufen werden. während des Baus oder der Zerstörung (12.6.2). Wenn eine virtuelle Funktion wird direkt oder indirekt von einem Konstruktor oder von einem .__ aufgerufen. Zerstörer, auch während des Baus oder der Zerstörung des Die nicht statischen Datenelemente der Klasse und das Objekt, für das der Aufruf aufgerufen wird gilt das Objekt (nennen es x) im Aufbau oder Zerstörung, Die aufgerufene Funktion ist der letzte Overrider im Konstruktor oder Destruktor-Klasse und nicht eine, die sie in einer abgeleiteten Klasse überschreibt .. Wenn der virtuelle Funktionsaufruf einen expliziten Klassenmitgliedszugriff verwendet (5.2.5) und der Objektausdruck verweist auf das vollständige Objekt von x oder eines der Basisklassen-Subobjekte dieses Objekts, jedoch nicht x oder eines seiner Basisklassen-Unterobjekte, das Verhalten ist undefined.

Rufen Sie also keine virtual-Funktionen von Konstruktoren oder Destruktoren auf, die versuchen, ein Objekt in Konstruktion oder Zerstörung aufzurufen. Die Konstruktionsreihenfolge beginnt mit Basis bis abgeleitete und die Reihenfolge der Destruktoren beginnt mit zur Basisklasse abgeleitet.

Der Aufruf einer abgeleiteten Klassenfunktion aus einer im Aufbau befindlichen Basisklasse ist daher gefährlich. Ähnlich wird ein Objekt in umgekehrter Reihenfolge von der Konstruktion zerstört. Wenn Sie also versuchen, eine Funktion in einer abgeleiteten Klasse von einem Destruktor aufzurufen, kann auf Ressourcen zugreifen, die bereits vorhanden sind wurde veröffentlicht.

1
M.S Chaudhari

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

Wenn Sie dem Basistyp einen Vorlagenkonstruktor hinzufügen, sodass das Vorlagenargument immer als abgeleiteter Typ abgeleitet wird, ist es möglich, den konkreten Typ des abgeleiteten Typs zu kennen. Von dort aus können Sie static - Memberfunktionen für diesen abgeleiteten Typ aufrufen.

Mit dieser Lösung können keine Nicht-Member-Funktionen static aufgerufen werden. Während sich die Ausführung im Konstruktor des Basistyps befindet, hatte der Konstruktor des abgeleiteten Typs noch nicht einmal Zeit, seine Elementinitialisierungsliste zu durchsuchen. Der abgeleitete Typteil der zu erstellenden Instanz wurde noch nicht initialisiert. Und da Funktionen von Nicht-Mitgliedern static mit ziemlicher Sicherheit mit Datenmitgliedern interagieren, ist es ungewöhnlich, dass want die Funktionen der Nicht-Mitglieder static des abgeleiteten Typs von den Funktionen des Basistyps aufruft Konstrukteur.

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 erstellt wird, hängt das Verhalten des Konstruktors Base vom tatsächlichen dynamischen Typ des zu erstellenden Objekts ab.

1

Kennen Sie den Absturzfehler von Windows Explorer ?! "Rein virtueller Funktionsaufruf ..."
Gleiches Problem ... 

class AbstractClass 
{
public:
    AbstractClass( ){
        //if you call pureVitualFunction I will crash...
    }
    virtual void pureVitualFunction() = 0;
};

Da für die Funktion pureVitualFunction () keine Implementierung erfolgt und die Funktion im Konstruktor aufgerufen wird, stürzt das Programm ab. 

1
TimW

Die vtables werden vom Compiler erstellt. Ein Klassenobjekt hat einen Zeiger auf seine vtable. Wenn der vtable-Zeiger mit dem Leben beginnt, zeigt er auf die vtable Der Basisklasse. Am Ende des Konstruktorcodes generiert der Compiler Code, um den Zeiger für die vtable auf die tatsächliche vtable für die Klasse zu verweisen. Dadurch wird sichergestellt, dass der Konstruktorcode, der virtuelle Funktionen aufruft, die Basisklassenimplementierungen dieser Funktionen aufruft, nicht die Überschreibung in der Klasse.

1
Yogesh

Wie bereits erwähnt, werden die Objekte beim Erstellen von Grund auf neu erstellt. Beim Erstellen des Basisobjekts ist das abgeleitete Objekt noch nicht vorhanden, sodass eine Überschreibung der virtuellen Funktion nicht funktionieren kann.

Dies kann jedoch mit polymorphen Gettern gelöst werden, die statischen Polymorphismus anstelle von virtuellen Funktionen verwenden, wenn Ihre Getter Konstanten zurückgeben, oder auf andere Weise in einem statischen Member ausgedrückt werden können Funktion, In diesem Beispiel wird CRTP ( https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern ) verwendet.

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;
}

Bei Verwendung des statischen Polymorphismus weiß die Basisklasse, welcher Klassen-Getter aufgerufen werden soll, da die Informationen zur Kompilierungszeit bereitgestellt werden.

0
stands2reason

Zuerst wird Object erstellt und dann weisen wir die Adresse den Zeigern zu. Konstruktoren werden zum Zeitpunkt der Objekterstellung aufgerufen und zur Initialisierung des Werts von Datenelementen verwendet. Der Zeiger auf das Objekt wird nach der Objekterstellung im Szenario angezeigt. Deshalb erlaubt uns C++ nicht, Konstruktoren als virtuell zu definieren. Ein weiterer Grund ist, dass es nichts wie einen Zeiger auf Konstruktor gibt, der auf virtuellen Konstruktor verweisen kann, da eine der Eigenschaften der virtuellen Funktion darin besteht nur von Zeigern verwendet werden. 

  1. Virtuelle Funktionen werden verwendet, um Werte dynamisch zuzuweisen, da Konstruktoren statisch sind. Daher können wir sie nicht virtuell machen. 
0
Priya