Appel des fonctions virtuelles à l’intérieur des 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 pourrait s’attendre à ce que n soit défini sur 2.

Il s’avère que n est défini sur 1. Pourquoi?

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 pas plus loin.

La FAQ C ++ Lite couvre cela dans la section 23.7 de manière assez détaillée. Je suggère de lire cela (et le rest de la FAQ) pour un suivi.

EDIT corrigé le plus à tous (merci litb)

L’appel d’une fonction polymorphe à partir d’un constructeur est une recette pour le désastre dans la plupart des langages OO. Différentes langues fonctionneront différemment lorsque cette situation est 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 ce que cela signifie d’appeler une méthode polymorphe du constructeur. Selon vous, comment se comporte-t-il? Il existe deux approches: appeler la méthode au niveau Base (style C ++) ou appeler la méthode polymorphe sur un object non construit en bas de la hiérarchie (méthode Java).

En C ++, la classe de base va construire sa version de la table de méthodes virtuelles avant d’entrer sa propre construction. À ce stade, un appel à la méthode virtuelle finira par appeler la version Base de la méthode ou produira une méthode virtuelle pure appelée au cas où elle ne serait pas implémentée à ce niveau de la hiérarchie. Une fois la base entièrement construite, le compilateur commence à construire la classe Derived et remplace 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éera l'équivalent de la 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 remplacée dans la classe dérivée, l'appel sera effectivement traité au niveau dérivé en appelant une méthode sur un object non construit, ce qui donnera des résultats inattendus. Tous les atsortingbuts de la classe dérivée qui sont initialisés dans le bloc constructeur ne sont pas encore initialisés, y compris les atsortingbuts «finaux». Les éléments qui ont 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( Ssortingng args[] ) { Derived d = new Derived( 5 ); } } // outputs: Derived 0 // Derived 5 // ... so much for final atsortingbutes never changing :P 

Comme vous le voyez, l'appel d'une méthode polymorphe ( virtuelle en terminologie C ++) est une source fréquente d'erreurs. En C ++, au moins vous avez la garantie qu'il n'appellera jamais une méthode sur un object non encore construit ...

La raison en est que les objects C ++ sont construits comme des oignons, de l’intérieur. Les super-classes sont construites avant les classes dérivées. Donc, avant de pouvoir créer un B, un A doit être créé. Lorsque le constructeur de A est appelé, ce n’est pas encore un B, donc la table de fonction virtuelle a toujours l’entrée pour la copie de A de fn ().

La FAQ C ++ Lite Couvre bien cela:

Essentiellement, lors de l’appel au constructeur des classes de base, l’object n’est pas encore du type dérivé et, par conséquent, l’implémentation du type de base de la fonction virtuelle est appelée et non du type dérivé.

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

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

   renvoyer pObject;
 }
  • Utilisez-le comme ceci:
 class MyClass: object public 
 {
 Publique:
   vide virtuel afterConstruction ()
   {
     // faire quelque chose.
   }
   // ...
 };

 MyClass * pMyObject = factoryNew ();

Connaissez-vous l’erreur de crash 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; }; 

Comme il n’y a pas d’implémentation pour la fonction pureVitualFunction () et que la fonction est appelée dans le constructeur, le programme se bloque.

Les vtables sont créées par le compilateur. Un object de classe a un pointeur vers sa vtable. Lorsqu’il démarre, ce pointeur vtable pointe sur la vtable de la classe de base. A la fin du code constructeur, le compilateur génère du code pour renvoyer le pointeur vtable vers la vtable réelle de la classe. Cela garantit que le code constructeur qui appelle les fonctions virtuelles appelle les implémentations de classe de base de ces fonctions, et non la substitution dans la classe.

Le standard C ++ (ISO / IEC 14882-2014) dit:

Les fonctions membres, y compris les fonctions virtuelles (10.3), peuvent être appelées pendant la construction ou la destruction (12.6.2). Lorsqu’une fonction virtuelle est appelée directement ou indirectement à partir d’un constructeur ou d’un destructeur, y compris pendant la construction ou la destruction des membres de données non statiques de la classe, et l’object auquel l’appel s’applique est l’object (appelez-le x) en construction ou destruction, la fonction appelée est le substitut final dans la classe du constructeur ou du destructeur et non une qui la remplace dans une classe plus dérivée. Si l’appel de fonction virtuelle utilise un access de membre de classe explicite (5.2.5) et que l’expression d’object se réfère à l’object complet de x ou à l’un des sous-objects de classe de base de cet object mais pas x ou l’un de ses sous-objects de classe de base, le comportement est indéfini .

Donc, n’invoquez pas virtual fonctions virtual des constructeurs ou des destructeurs qui tentent d’appeler l’object en construction ou en destruction, car l’ordre de construction commence de base à dérivé et l’ordre des destructeurs commence à partir de la classe de base .

Ainsi, tenter d’appeler une fonction de classe dérivée à partir d’une classe de base en construction est dangereux. De même, un object est détruit dans l’ordre inverse de la construction, donc tenter d’appeler une fonction d’une classe plus dérivée peut accéder à des ressources déjà été libéré.

Premièrement, Object est créé, puis nous lui assignons son adresse. Les constructeurs sont appelés au moment de la création de l’object et utilisés pour initialiser la valeur des membres de données. Le pointeur sur l’object entre en scène après la création de l’object. C’est pourquoi, C ++ ne nous permet pas de rendre les constructeurs virtuels. Une autre raison est que, il n’y a rien de tel que le pointeur vers le constructeur, qui peut pointer vers le constructeur virtuel, car l’une des propriétés de la fonction virtuelle est qu’elle ne peut être utilisée que par des pointeurs.

  1. Les fonctions virtuelles sont utilisées pour atsortingbuer une valeur dynamic, car les constructeurs sont statiques, nous ne pouvons donc pas les rendre virtuels.

Comme cela a été souligné, les objects sont créés de bas en bas lors de la construction. Lorsque l’object de base est en cours de construction, l’object dérivé n’existe pas encore, de sorte qu’une substitution de fonction virtuelle ne peut pas fonctionner.

Cependant, ceci peut être résolu avec des getters polymorphes qui utilisent un polymorphism statique au lieu de fonctions virtuelles si vos constantes retournent des constantes ou peuvent être exprimées dans une fonction membre statique. Cet exemple utilise CRTP ( https://en.wikipedia.org/wiki / Curiously_recurring_template_pattern ).

 template class Base { public: inline Base() : foo(DerivedClass::getFoo()) {} inline int fooSq() { return foo * foo; } const int foo; }; class A : public Base { public: inline static int getFoo() { return 1; } }; class B : public Base { public: inline static int getFoo() { return 2; } }; class C : public Base { 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 polymorphism statique, la classe de base sait quelle classe de getter appeler lorsque les informations sont fournies à la compilation.

Je ne vois pas l’importance du mot clé virtuel ici. b est une variable de type statique, et son type est déterminé par le compilateur au moment de la compilation. Les appels de fonction ne feraient pas référence à la vtable. Lorsque b est construit, le constructeur de sa classe parente est appelé, ce qui explique pourquoi la valeur de _n est définie sur 1.

Au cours de l’appel du constructeur de l’object, la table de pointeurs de fonction virtuelle n’est pas complètement construite. Cela ne vous donnera généralement pas le comportement que vous attendez. L’appel d’une fonction virtuelle dans cette situation peut fonctionner mais n’est pas garanti et doit être évité pour être portable et respecter la norme C ++.