Résolution d'appel virtuel
Nous allons maintenant rentrer dans les détails de ce qu’il se passe sous le manteau, lorsque le programme effectue un appel virtuel.
Virtual table
Lorsqu’une classe Parent
contenant des fonctions virtuelles est compilée, le compilateur va générer pour l’ensemble des classes qui en dérivent une virtual table.
Il s’agit d’une tableau qui contient, pour chaque fonction virtuelle de la classe, un pointeur sur la fonction qui sera vraiment appelée.
Analysons la hiérarchie suivante :
struct GrandFather
{
virtual ~GrandFather() {}
virtual void fcn_a(bool);
void fcn_b();
virtual void fcn_c() const;
};
struct Father : GrandFather
{
void fcn_a(bool);
void fcn_b();
virtual void fcn_d();
};
struct Son : Father
{
void fcn_a(bool);
void fcn_c();
};
struct GrandSon : Son
{
void fcn_a(char, int);
void fcn_c() const;
void fcn_d();
};
Nous avons volontairement omis les override
, car le but est d’analyser le comportement du programme pendant un appel de fonction.
Bien entendu, dans un vrai programme, vous devez toujours placer override
sur une fonction redéfinie.
Nous allons commencer par déterminer le contenu de la virtual table de Father
.
Listons d’abord l’ensemble des fonctions virtuelles de son parent GrandFather
:
- void fcn_a(bool)
- void fcn_c() const
Pour chacune de ces fonctions, on regarde dans la classe courante si on trouve une déclaration avec la même signature. C’est le cas pour fcn_a
, mais pas pour fcn_c
.
La virtual table de Father
sera donc la suivante :
- void fcn_a(bool) -> void Father::fcn_a(bool)
- void fcn_c() const -> void GrandFather::fcn_c() const
.
Réalisons maintenant le même travail pour Son
et GrandSon
.
Fonctions virtuelles dans la classe Father
, parent de Son
:
- void fcn_a(bool)
- void fcn_c() const
- void fcn_d();
Pour avoir toutes les fonctions virtuelles de Father
, on prend toutes les fonctions virtuelles de son propre parent GrandFather
et on ajoute les fonctions marquées virtuelles dans Father
.
Virtual table de Son
:
- void fcn_a(bool) -> void Son::fcn_a(bool)
- void fcn_c() const -> void GrandFather::fcn_c() const
(car Son::fcn_c
n’est pas const)
- void fcn_d() -> void Father::fcn_d()
Fonctions virtuelles dans la classe Son
, parent de GrandSon
:
- void fcn_a(bool)
- void fcn_c() const
- void fcn_d();
Virtual table de GrandSon
:
- void fcn_a(bool) -> void Son::fcn_a(bool)
(car GrandSon::fcn_a
n’a pas le bon nombre de paramètres)
- void fcn_c() const -> void GrandSon::fcn_c() const
- void fcn_d() -> void GrandSon::fcn_d()
Résolution d’appel
Maintenant que l’on connaît les virtual table, nous allons pouvoir déterminer la fonction appelée sur un objet en fonction de son type statique (= type de la variable ou de l’expression) et de son type dynamique (= type avec lequel l’objet est construit).
Son real_son;
GrandSon real_grand_son;
Father real_father;
GrandFather real_grand_father;
Father& son_as_father = real_son;
Father& grand_son_as_father = real_grand_son;
GrandFather& son_as_grand_father = real_son;
GrandFather& father_as_grand_father = real_father;
Considérons l’objet real_father
.
Le type statique est Father
, puisque la variable est de type Father
.
Le type dynamique est Father
, puisqu’il a été construit en tant que Father
.
Considérons maintenant son_as_father
.
Le type statique est toujours Father
, puisque la variable est de type Father&
.
En revanche, le type dynamique est Son
, puisque l’objet référencé par son_as_father
est real_son
, qui a été construit en tant que Son
.
La résolution d’un appel de fonction se fait en 3 étapes :
- Le compilateur recherche la fonction appelée dans le type statique de l’objet. S’il ne trouve pas la fonction, il remonte dans la classe du parent, et ainsi de suite.
- Une fois cette fonction trouvée, il regarde si elle est virtuelle ou non.
- Si oui, alors la résolution de l’appel est finalisée à l’exécution, en utilisant la virtual table du type dynamique de l’objet.
Si non, alors la résolution de l’appel s’achève pendant la compilation.
Essayons de prédire ce qu’il va se produire à l’exécution sur quelques exemples.
son_as_grand_father.fcn_b();
- Le type statique de
son_as_grand_father
estGrandFather
. Dans la classeGrandFather
, on trouve la fonctionfcn_b()
. - Celle-ci n’est pas virtuelle.
- Le compilateur décide que l’instruction appelera
GrandFather::fcn_b()
.
son_as_father.fcn_b();
- Le type statique de
son_as_grand_father
estFather
. Dans la classeFather
, on trouve la fonctionfcn_b()
. - Celle-ci n’est pas virtuelle, puisqu’elle n’est pas référencée dans la virtual table de
Father
. - Le compilateur décide que l’instruction appelera
Father::fcn_b()
.
son_as_grand_father.fcn_d();
- Le type statique de
son_as_grand_father
estGrandFather
. Dans la classeGrandFather
, on ne trouve pas de fonction fonctionfcn_d()
.
=> L’instruction ne compile pas.
son_as_father.fcn_d();
- Le type statique de
son_as_father
estFather
. Dans la classeFather
, on trouve la fonctionfcn_d()
. - Celle-ci est virtuelle.
- Au moment de l’exécution, le type dynamique de
son_as_father
estSon
. Dans sa virtual table,fcn_d()
pointe surFather::fcn_d()
, c’est donc cette fonction là qui sera appelée.
grand_son_as_father.fcn_d();
- Le type statique de
grand_son_as_father
estFather
. Dans la classeFather
, on trouve la fonctionfcn_d()
. - Celle-ci est virtuelle.
- Au moment de l’exécution, le type dynamique de
grand_son_as_father
estGrandSon
. Dans sa virtual table,fcn_d()
pointe surGrandSon::fcn_d()
, c’est donc cette fonction là qui sera appelée.
A vous de jouer
Essayez maintenant de déterminer par vous-mêmes les fonctions qui seront appelées par les instructions ci-dessous.
Father& grand_son_as_father = grand_son;
grand_son_as_father.fcn_a('a', 8) // (1)
GrandSon grand_son;
grand_son.fcn_a(false); // (2)
grand_son_as_father.fcn_c(); // (3)
Son son;
const Son& son_cref = son;
son_cref.fcn_c(); // (4)
GrandFather& son_as_grand_father = son;
son_as_grand_father.fcn_c(); // (5)
Father grand_son_copy_as_father = grand_son;
grand_son_copy_as_father.fcn_d(); // (6)
Attention, il y a des vilains pièges, donc vérifiez bien la solution à la fin !