256 votes

Appel de fonctions virtuelles dans les 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 est réglé sur 2.

Il s'avère que n est fixé à 1. Pourquoi ?

8 votes

Je pose ma propre question et j'y réponds parce que je veux que l'explication de cette partie de l'ésotérisme du C++ figure dans Stack Overflow. Une version de ce problème a frappé notre équipe de développement deux fois, donc je suppose que cette information pourrait être utile à quelqu'un là-bas. Veuillez rédiger une réponse si vous pouvez l'expliquer d'une manière différente/meilleure...

8 votes

Je me demande pourquoi il a été rejeté ? Quand j'ai appris le C++ pour la première fois, cela m'a vraiment dérouté. +1

3 votes

Ce qui me surprend, c'est l'absence d'avertissement du compilateur. Le compilateur substitue un appel à la "fonction définie dans la classe du constructeur actuel" pour ce qui serait dans tout autre cas la fonction "la plus surchargée" dans une classe dérivée. Si le compilateur disait "substituer Base::foo() à l'appel à la fonction virtuelle foo() dans le constructeur", le programmeur serait averti que le code ne fera pas ce qu'il attend. Cela serait beaucoup plus utile que de faire une substitution silencieuse, conduisant à un comportement mystérieux, beaucoup de débogage, et finalement un voyage à stackoverflow pour l'éclaircissement.

238voto

JaredPar Points 333733

L'appel de fonctions virtuelles à partir d'un constructeur ou d'un destructeur est dangereux et doit être évité dans la mesure du 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 au-delà.

Le site FAQ C++ Lite couvre ce sujet dans la section 23.7 de manière assez détaillée. Je vous suggère de la lire (ainsi que le reste de la FAQ) pour un suivi.

Extrait :

[...] Dans un constructeur, le mécanisme d'appel virtuel est désactivé parce que la surcharge des classes dérivées n'a pas encore eu lieu. Les objets sont construits à partir de la base, "la base avant les dérivés".

[...]

La destruction se fait "la classe dérivée avant la classe de base", donc les fonctions virtuelles se comportent comme des constructeurs : Seules les définitions locales sont utilisées - et aucun appel n'est fait aux fonctions de surcharge pour éviter de toucher la partie de l'objet qui appartient à la classe dérivée (maintenant détruite).

EDIT Corrigé Most to All (merci litb)

0 votes

J'aimerais pouvoir voter 100 fois plus haut. Faire de telles choses indique généralement une conception défectueuse.

59 votes

Pas la plupart des implémentations C++, mais toutes les implémentations C++ doivent appeler la version de la classe courante. Si certaines ne le font pas, alors elles ont un bug :). Je suis toujours d'accord avec vous qu'il est mauvais d'appeler une fonction virtuelle à partir d'une classe de base - mais la sémantique est précisément définie.

2 votes

Et la page suivante de la FAQ discute exactement de la façon de le contourner.

85voto

L'appel d'une fonction polymorphe à partir d'un constructeur est une recette pour un désastre dans la plupart des langages OO. Les différents langages se comportent différemment lorsque cette situation se présente.

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

En C++, la classe de base construit sa version de la table des méthodes virtuelles avant d'entrer dans sa propre construction. À ce stade, un appel à la méthode virtuelle finira par appeler la version de la méthode de la classe de base ou par produire un message de type une méthode virtuelle pure appelée au cas où il n'y aurait pas d'implémentation à ce niveau de la hiérarchie. Après la construction complète de la base, le compilateur commencera à construire la classe dérivée, et il 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 construit 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 à mon avis plus dangereuses). Si le constructeur de la classe de base appelle une méthode qui est surchargée dans la classe dérivée, l'appel sera en fait traité au niveau dérivé en appelant une méthode sur un objet non construit, ce qui donne des résultats inattendus. Tous les attributs de la classe dérivée qui sont initialisés dans le bloc constructeur ne sont pas encore initialisés, y compris les attributs "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( String args[] ) {
      Derived d = new Derived( 5 );
   }
}
// outputs: Derived 0
//          Derived 5
// ... so much for final attributes never changing :P

Comme vous pouvez le constater, l'appel d'une fonction polymorphe ( virtuel dans la terminologie du C++) est une source courante d'erreurs. En C++, vous avez au moins la garantie que l'on n'appellera jamais une méthode sur un objet encore non construit...

1 votes

Bon travail pour expliquer pourquoi l'alternative est (aussi) source d'erreurs.

0 votes

"Si le constructeur de la classe de base appelle une méthode qui est surchargée dans la classe dérivée, l'appel sera en fait traité au niveau dérivé en appelant une méthode sur un objet non construit..." Comment si la base est déjà initialisée. Il n'y a aucune possibilité à moins que vous n'appeliez explicitement "init" avant d'initialiser les autres membres.

3 votes

Une explication ! +1, réponse supérieure imho

63voto

David Coufal Points 1388

La raison en est que les objets C++ sont construits comme des oignons, de l'intérieur vers l'extérieur. Les classes de base sont construites avant les classes dérivées. Ainsi, avant qu'un B puisse être construit, un A doit être construit. Lorsque le constructeur de A est appelé, ce n'est pas encore un B, donc la table des fonctions virtuelles contient toujours l'entrée pour la copie de fn() de A.

17 votes

Le C++ n'utilise normalement pas le terme "super classe" - il préfère "classe de base".

0 votes

C'est la même chose dans la plupart des langages OO : vous ne pouvez pas construire un objet dérivé sans que la partie de base soit déjà construite.

2 votes

@DavidRodríguez-dribeas D'autres langages le font effectivement. Par exemple, en Pascal, la mémoire est d'abord allouée pour l'ensemble de l'objet, puis seul le constructeur le plus dérivé est invoqué. Un constructeur doit soit contenir un appel explicite au constructeur de son parent (qui ne doit pas nécessairement être la première action - il doit juste être quelque part), ou s'il ne le fait pas, c'est comme si la première ligne du constructeur faisait cet appel.

28voto

Aaron Maenpaa Points 39173

Le site FAQ C++ Lite Couvre ça plutôt bien :

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

3 votes

Une réponse claire, directe et simple. C'est quand même une fonction que j'aimerais bien voir s'améliorer. Je déteste avoir à écrire toutes ces fonctions idiotes initializeObject() que l'utilisateur est obligé d'appeler juste après la construction, ce n'est pas très bien vu pour un cas d'utilisation très courant. Je comprends cependant la difficulté. C'est la vie.

1 votes

@moodboom Quel "amour" proposez-vous ? Gardez à l'esprit que vous ne pouvez pas simplement changer la façon dont les choses fonctionnent actuellement en place, car cela casserait horriblement des rames de code existant. Alors, comment feriez-vous à la place ? Non seulement quelle nouvelle syntaxe vous introduiriez pour permettre les appels virtuels (réels, non dévirtualisés) dans les constructeurs - mais aussi comment vous modifieriez d'une manière ou d'une autre les modèles de construction/durée de vie des objets pour que ces appels aient un objet complet du type dérivé sur lequel s'exécuter. Ce sera intéressant.

0 votes

@underscore_d Je ne pense pas qu'un changement de syntaxe soit nécessaire. Peut-être que lors de la création d'un objet, le compilateur ajouterait du code pour parcourir la vtable et rechercher ce cas et patcher les choses ensuite ? Je n'ai jamais écrit un compilateur C++ et je suis sûr que mon commentaire initial pour donner un peu d'"amour" était naïf et que cela n'arrivera jamais :-) Une fonction virtuelle initialize() n'est pas une solution très douloureuse de toute façon, vous devez juste vous rappeler de l'appeler après avoir créé votre objet.

16voto

Tobias Points 3120

Une solution à votre problème consiste à utiliser des méthodes de fabrique 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: virtual void afterConstruction() {} // ... };

  • Définir une méthode d'usine :

    template< class C > C* factoryNew() { C* pObject = new C(); pObject->afterConstruction();

    return pObject; }

  • Utilisez-le comme ça :

    class MyClass : public Object { public: virtual void afterConstruction() { // do something. } // ... };

    MyClass* pMyObject = factoryNew();

0 votes

Type à spécifier pour la fonction modèle MyClass* pMyObject = factoryNew<MyClass>() ;

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X