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...
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.
0 votes
@CraigReynolds Pas nécessairement. Il n'y a pas besoin d'un traitement spécial du compilateur pour les appels virtuels à l'intérieur des constructeurs. Le constructeur de la classe de base crée la vtable pour la classe actuelle seulement, donc à ce moment-là le compilateur peut juste appeler la fonction virtuelle via cette vtable exactement de la même manière que d'habitude. Mais la vtable ne pointe pas encore vers une fonction dans une classe dérivée. La vtable pour la classe dérivée est ajustée par le constructeur de la classe dérivée après le retour du constructeur de la classe de base, ce qui est la façon dont la surcharge fonctionnera une fois que la classe dérivée est construite.