web-dev-qa-db-fra.com

Appel de fonctions virtuelles à l'intérieur de constructeurs

Supposons que j'ai deux classes C++:

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

Si j'écris le code suivant:

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

On peut s’attendre à ce que n soit défini sur 2.

Il se trouve que n est défini sur 1. Pourquoi?

200
David Coufal

L'appel de fonctions virtuelles à partir d'un constructeur ou d'un destructeur est dangereux et doit être évité autant que possible. Toutes les implémentations C++ doivent appeler la version de la fonction définie au niveau de la hiérarchie dans le constructeur actuel et non plus. 

Le C++ FAQ Lite couvre cela dans la section 23.7 avec assez de détails. Je suggère de lire cela (et le reste de la FAQ) pour un suivi.

Extrait:

[...] Dans un constructeur, le mécanisme d’appel virtuel est désactivé car la substitution à partir de classes dérivées n’a pas encore eu lieu. Les objets sont construits à partir de la base, «base avant dérivée».

[...]

La destruction est effectuée «classe dérivée avant la classe de base», donc les fonctions virtuelles se comportent comme dans les constructeurs: seules les définitions locales sont utilisées - et aucun appel n'est effectué vers les fonctions de substitution pour éviter de toucher à la partie de la classe dérivée (maintenant détruite).

EDIT Corrigé le plus à tous (merci litb)

186
JaredPar

L'appel d'une fonction polymorphe à partir d'un constructeur est une recette pour un sinistre dans la plupart des langages OO. Différentes langues fonctionneront différemment lorsque cette situation sera rencontrée.

Le problème fondamental est que, dans toutes les langues, le ou les types de base doivent être construits avant le type dérivé. Maintenant, le problème est de savoir ce que signifie appeler une méthode polymorphe à partir du constructeur. Comment vous attendez-vous à ce qu'il se comporte? Il existe deux approches: appeler la méthode au niveau Base (style C++) ou appeler la méthode polymorphe sur un objet non construit au bas de la hiérarchie (méthode Java).

En C++, la classe Base construira sa version de la table de méthode virtuelle avant d'entrer dans sa propre construction. À ce stade, un appel à la méthode virtuelle finira par appeler la version de base de la méthode ou en produisant une méthode virtuelle pure appelée si elle n’a aucune implémentation à ce niveau de la hiérarchie. Une fois la base entièrement construite, le compilateur commencera à construire la classe dérivée et remplacera les pointeurs de méthode pour pointer vers les implémentations du niveau suivant de la hiérarchie.

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

En Java, le compilateur crée l'équivalent de table virtuelle à la toute première étape de la construction, avant d'entrer dans le constructeur de base ou le constructeur dérivé. Les implications sont différentes (et à mes goûts plus dangereux). Si le constructeur de la classe de base appelle une méthode annulée dans la classe dérivée, l'appel sera en réalité traité au niveau dérivé en appelant une méthode sur un objet non construit, ce qui produira des résultats inattendus. Tous les attributs de la classe dérivée initialisés à l'intérieur du bloc constructeur ne sont pas encore initialisés, y compris les attributs "finaux". Les éléments ayant une valeur par défaut définie au niveau de la classe auront cette valeur.

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

Comme vous le voyez, l’appel à une méthode polymorphe ( virtual in C++) est une source commune d’erreurs. En C++, vous avez au moins la garantie qu'il n'appellera jamais une méthode sur un objet encore non construit ...

La raison en est que les objets C++ sont construits comme des oignons, de l'intérieur. Les super-classes sont construites avant les classes dérivées. Donc, avant qu'un B puisse être créé, un A doit être créé. Lorsque le constructeur de A est appelé, il ne s'agit pas encore d'un B, de sorte que la table de fonction virtuelle contient toujours l'entrée correspondant à la copie de fn () de A.

54
David Coufal

Le C++ FAQ Lite Couvre cela plutôt bien:

Essentiellement, lors de l'appel du constructeur de classes de base, l'objet n'est pas encore du type dérivé et l'implémentation de la fonction virtuelle du type de base est donc appelée et non du type dérivé.

22
Aaron Maenpaa

Une solution à votre problème consiste à utiliser des méthodes d'usine pour créer votre objet.

  • Définissez une classe de base commune pour votre hiérarchie de classes contenant une méthode virtuelle afterConstruction ():
 class object 
 {
 public: 
 void virtuel afterConstruction () {} 
 // ...
}; 
  • Définir une méthode d'usine:
 template <classe C> 
 C * factoryNew () 
 {
 C * pObject = new C (); 
 pObject-> afterConstruction (); 

 return pObject; 
} 
  • Utilisez-le comme ceci:
 class MyClass: objet public 
 {
 public: 
 void virtual afterConstruction () 
 {
 // faire quelque chose.
 } 
 // ...
}; 

 MyClass * pMyObject = factoryNew (); 

13
Tobias

La norme C++ (ISO/IEC 14882-2014) say's:

Les fonctions membres, y compris les fonctions virtuelles (10.3), peuvent être appelées pendant la construction ou la destruction (12.6.2). Quand une fonction virtuelle est appelé directement ou indirectement par un constructeur ou par un destructeur, y compris lors de la construction ou de la destruction du Les membres de données non statiques de la classe et l’objet pour lequel l’appel est appelé s'applique est l'objet (appelez-le x) en construction ou en destruction, la fonction appelée est l’ultime surchargeur dans le constructeur ou la classe du destructeur et pas celle qui la remplace dans une classe plus dérivée . Si l'appel de fonction virtuelle utilise un accès explicite aux membres de la classe (5.2.5) et l'expression de l'objet fait référence à l'objet complet de x ou l’un des sous-objets de la classe de base de cet objet, mais pas x ni l’un de ses sous-objets de classe de base, le comportement est undefined.

Donc, n'appelle pas les fonctions virtual de constructeurs ou de destructeurs qui tentent d'appeler l'objet en construction ou en destruction, car l'ordre de construction commence de base à dérivé et l'ordre des destructeurs commence de dérivé à base classe.

Il est donc dangereux d'essayer d'appeler une fonction de classe dérivée à partir d'une classe de base en construction. De la même manière, un objet est détruit dans l'ordre inverse de la construction; été libéré.

1
M.S Chaudhari

D'autres réponses ont déjà expliqué pourquoi les appels de fonction virtual ne fonctionnent pas comme prévu lorsqu'ils sont appelés depuis un constructeur. J'aimerais plutôt proposer un autre moyen de contourner le problème pour obtenir un comportement de type polymorphe à partir du constructeur d'un type de base.

En ajoutant un constructeur de modèle au type de base, de sorte que l'argument de modèle soit toujours considéré comme étant le type dérivé, il est possible de connaître le type concret du type dérivé. À partir de là, vous pouvez appeler les fonctions membres static pour ce type dérivé.

Cette solution ne permet pas d'appeler des fonctions membres non -static. Bien que l'exécution se trouve dans le constructeur du type de base, le constructeur du type dérivé n'a même pas eu le temps de parcourir la liste d'initialisation de ses membres. La partie de type dérivé de l'instance en cours de création n'a pas commencé à être initialisée. Et comme les fonctions membres non -static interagissent presque certainement avec les membres de données, il serait inhabituel de vouloir d'appeler les fonctions membres non -static du type dérivé à partir du constructeur du type de base.

Voici un exemple d'implémentation:

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

Cet exemple devrait imprimer 

Derived created
Derived destroyed
Base created
Base destroyed

Lors de la construction de Derived, le comportement du constructeur Base dépend du type dynamique réel de l'objet en cours de construction.

1
François Andrieux

Connaissez-vous l'erreur de plantage de l'explorateur Windows?! "Appel de fonction virtuelle pure ..."
Même problème ... 

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

Parce qu'il n'y a pas d'implémentation pour la fonction pureVitualFunction () et que la fonction est appelée dans le constructeur, le programme va planter. 

1
TimW

Les vtables sont créés par le compilateur. Un objet de classe a un pointeur sur sa vtable. Quand il commence sa vie, ce pointeur vtable pointe sur la vtable De la classe de base. À la fin du code constructeur, le compilateur génère un code permettant de re-pointer le pointeur vtable vers la vtable réelle de la classe. Cela garantit que le code de constructeur qui appelle des fonctions virtuelles appelle les implémentations de la classe de base Base de ces fonctions, et non le remplacement dans la classe.

1
Yogesh

Comme il a été souligné, les objets sont créés à la base. Lorsque l'objet de base est en cours de construction, l'objet dérivé n'existe pas encore. Un remplacement de fonction virtuelle ne peut donc pas fonctionner.

Cependant, cela peut être résolu avec des getters polymorphes qui utilisent static polymorphism au lieu de fonctions virtuelles si vos getters renvoient des constantes, ou qui peuvent sinon être exprimés dans une fonction membre statique. Cet exemple utilise 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;
}

Avec l'utilisation du polymorphisme statique, la classe de base sait quel getter de la classe à appeler lorsque les informations sont fournies au moment de la compilation.

0
stands2reason

Tout d'abord, Object est créé, puis nous l'attribuons à des pointeurs. Les constructeurs sont appelés au moment de la création de l'objet et utilisés pour initialiser la valeur des membres de données. Le pointeur sur l'objet entre dans le scénario après la création de l'objet. C’est pourquoi, C++ ne nous permet pas de créer des constructeurs en tant que virtuels. Une autre raison est qu’il n’existe aucun point de repère sur constructeur qui puisse pointer sur un constructeur virtuel, être utilisé que par des pointeurs. 

  1. Les fonctions virtuelles permettent d’attribuer des valeurs de façon dynamique, les constructeurs étant statiques, nous ne pouvons pas les rendre virtuelles. 
0
Priya