virtual
function specifier
Spécifie qu'une fonction membre non statique est virtual et prend en charge la distribution dynamique. Elle ne peut apparaître que dans la decl-specifier-seq de la déclaration initiale d'une fonction membre non statique (c'est-à-dire lorsqu'elle est déclarée dans la définition de la classe).
Table des matières |
Explication
Les fonctions virtuelles sont des fonctions membres dont le comportement peut être redéfini dans les classes dérivées. Contrairement aux fonctions non virtuelles, le comportement de redéfinition est préservé même en l'absence d'informations à la compilation sur le type réel de la classe. Autrement dit, si une classe dérivée est manipulée via un pointeur ou une référence vers la classe de base, un appel à une fonction virtuelle redéfinie invoquera le comportement défini dans la classe dérivée. Un tel appel de fonction est appelé
appel de fonction virtuelle
ou
appel virtuel
. L'appel de fonction virtuelle est supprimé si la fonction est sélectionnée via une
recherche de nom qualifiée
(c'est-à-dire si le nom de la fonction apparaît à droite de l'opérateur de résolution de portée
::
).
#include <iostream> struct Base { virtual void f() { std::cout << "base\n"; } }; struct Derived : Base { void f() override // 'override' est optionnel { std::cout << "derived\n"; } }; int main() { Base b; Derived d; // appel de fonction virtuelle par référence Base& br = b; // le type de br est Base& Base& dr = d; // le type de dr est Base& également br.f(); // affiche "base" dr.f(); // affiche "derived" // appel de fonction virtuelle par pointeur Base* bp = &b; // le type de bp est Base* Base* dp = &d; // le type de dp est Base* également bp->f(); // affiche "base" dp->f(); // affiche "derived" // appel de fonction non virtuelle br.Base::f(); // affiche "base" dr.Base::f(); // affiche "base" }
En détail
Si une fonction membre
vf
est déclarée comme
virtual
dans une classe
Base
, et qu'une classe
Derived
, qui est dérivée, directement ou indirectement, de
Base
, possède une déclaration pour une fonction membre avec la même
- nom
- liste des types de paramètres (mais pas le type de retour)
- qualificateurs cv
- qualificateurs de référence
Alors cette fonction dans la classe
Derived
est également
virtual
(que le mot-clé
virtual
soit utilisé ou non dans sa déclaration) et
overrides
Base::vf (que le spécificateur
override
soit utilisé ou non dans sa déclaration).
Base::vf
n'a pas besoin d'être accessible ou visible pour être redéfini. (
Base::vf
peut être déclaré privé, ou
Base
peut être hérité en utilisant l'héritage privé. Les membres de même nom dans une classe de base de
Derived
qui hérite
Base
n'ont pas d'importance pour la détermination de la redéfinition, même s'ils masqueraient
Base::vf
lors de la recherche de nom.)
class B { virtual void do_f(); // membre privé public: void f() { do_f(); } // interface publique }; struct D : public B { void do_f() override; // redéfinit B::do_f }; int main() { D d; B* bp = &d; bp->f(); // appelle en interne D::do_f(); }
Pour chaque fonction virtuelle, il existe le
final overrider
, qui est exécuté lorsqu'un appel de fonction virtuelle est effectué. Une fonction membre virtuelle
vf
d'une classe de base
Base
est le final overrider sauf si la classe dérivée déclare ou hérite (via l'héritage multiple) une autre fonction qui remplace
vf
.
struct A { virtual void f(); }; // A::f est virtuelle struct B : A { void f(); }; // B::f remplace A::f dans B struct C : virtual B { void f(); }; // C::f remplace A::f dans C struct D : virtual B {}; // D n'introduit pas de remplacement, B::f est final dans D struct E : C, D // E n'introduit pas de remplacement, C::f est final dans E { using A::f; // pas une déclaration de fonction, rend simplement A::f visible pour la recherche }; int main() { E e; e.f(); // appel virtuel appelle C::f, le remplacement final dans e e.E::f(); // appel non virtuel appelle A::f, qui est visible dans E }
Si une fonction a plus d'un final overrider, le programme est mal formé :
struct A { virtual void f(); }; struct VB1 : virtual A { void f(); // remplace A::f }; struct VB2 : virtual A { void f(); // remplace A::f }; // struct Error : VB1, VB2 // { // // Erreur : A::f a deux remplacements finaux dans Error // }; struct Okay : VB1, VB2 { void f(); // OK : ceci est le remplacement final pour A::f }; struct VB1a : virtual A {}; // ne déclare pas de remplacement struct Da : VB1a, VB2 { // dans Da, le remplacement final de A::f est VB2::f };
Une fonction portant le même nom mais avec une liste de paramètres différente ne remplace pas la fonction de base du même nom, mais la masque : lorsque la recherche de nom non qualifiée examine la portée de la classe dérivée, la recherche trouve la déclaration et n'examine pas la classe de base.
struct B { virtual void f(); }; struct D : B { void f(int); // D::f masque B::f (liste de paramètres incorrecte) }; struct D2 : D { void f(); // D2::f remplace B::f (peu importe qu'elle ne soit pas visible) }; int main() { B b; B& b_as_b = b; D d; B& d_as_b = d; D& d_as_d = d; D2 d2; B& d2_as_b = d2; D& d2_as_d = d2; b_as_b.f(); // appelle B::f() d_as_b.f(); // appelle B::f() d2_as_b.f(); // appelle D2::f() d_as_d.f(); // Erreur : la recherche dans D ne trouve que f(int) d2_as_d.f(); // Erreur : la recherche dans D ne trouve que f(int) }
|
Si une fonction est déclarée avec le spécificateur
struct B { virtual void f(int); }; struct D : B { virtual void f(int) override; // OK, D::f(int) redéfinit B::f(int) virtual void f(long) override; // Erreur : f(long) ne redéfinit pas B::f(int) };
Si une fonction est déclarée avec le spécificateur
struct B { virtual void f() const final; }; struct D : B { void f() const; // Erreur : D::f tente de redéfinir B::f déclarée final }; |
(depuis C++11) |
Les fonctions non membres et les fonctions membres statiques ne peuvent pas être virtuelles.
Les modèles de fonction ne peuvent pas être déclarés
virtual
. Ceci s'applique uniquement aux fonctions qui sont elles-mêmes des modèles - une fonction membre régulière d'un modèle de classe peut être déclarée virtuelle.
|
Les fonctions virtuelles (qu'elles soient déclarées virtuelles ou qu'elles en redéfinissent une) ne peuvent avoir aucune contrainte associée. struct A { virtual void f() requires true; // Error: constrained virtual function };
Une fonction virtuelle
|
(depuis C++20) |
Les arguments par défaut pour les fonctions virtuelles sont substitués au moment de la compilation.
Types de retour covariants
Si la fonction
Derived::f
remplace une fonction
Base::f
, leurs types de retour doivent être identiques ou être
covariants
. Deux types sont covariants s'ils satisfont à toutes les exigences suivantes :
- les deux types sont des pointeurs ou des références (lvalue ou rvalue) vers des classes. Les pointeurs ou références multi-niveaux ne sont pas autorisés.
-
la classe référencée/pointée dans le type de retour de
Base::f()doit être une classe de base directe ou indirecte non ambiguë et accessible de la classe référencée/pointée du type de retour deDerived::f(). -
le type de retour de
Derived::f()doit être aussi ou moins cv-qualifié que le type de retour deBase::f().
La classe dans le type de retour de
Derived::f
doit être soit
Derived
elle-même, soit être un
type complet
au point de déclaration de
Derived::f
.
Lorsqu'un appel de fonction virtuelle est effectué, le type retourné par le final overrider est implicitement converti vers le type de retour de la fonction surchargée qui a été appelée :
class B {}; struct Base { virtual void vf1(); virtual void vf2(); virtual void vf3(); virtual B* vf4(); virtual B* vf5(); }; class D : private B { friend struct Derived; // dans Derived, B est une base accessible de D }; class A; // une classe déclarée à l'avance est un type incomplet struct Derived : public Base { void vf1(); // virtuel, remplace Base::vf1() void vf2(int); // non-virtuel, masque Base::vf2() // char vf3(); // Erreur : remplace Base::vf3, mais a un type de retour // différent et non covariant D* vf4(); // remplace Base::vf4() et a un type de retour covariant // A* vf5(); // Erreur : A est un type incomplet }; int main() { Derived d; Base& br = d; Derived& dr = d; br.vf1(); // appelle Derived::vf1() br.vf2(); // appelle Base::vf2() // dr.vf2(); // Erreur : vf2(int) masque vf2() B* p = br.vf4(); // appelle Derived::vf4() et convertit le résultat en B* D* q = dr.vf4(); // appelle Derived::vf4() et ne convertit pas le résultat en B* }
Destructeur virtuel
Bien que les destructeurs ne soient pas hérités, si une classe de base déclare son destructeur
virtual
, le destructeur dérivé le remplace toujours. Cela permet de supprimer des objets alloués dynamiquement de type polymorphe via des pointeurs vers la base.
class Base { public: virtual ~Base() { /* libère les ressources de Base */ } }; class Derived : public Base { ~Derived() { /* libère les ressources de Derived */ } }; int main() { Base* b = new Derived; delete b; // Effectue un appel de fonction virtuelle à Base::~Base() // puisqu'elle est virtuelle, elle appelle Derived::~Derived() qui peut // libérer les ressources de la classe dérivée, puis appelle // Base::~Base() suivant l'ordre habituel de destruction }
De plus, si le destructeur de la classe de base n'est pas virtuel, supprimer un objet de la classe dérivée via un pointeur vers la classe de base est un comportement indéfini , qu'il y ait ou non des ressources qui seraient fuitées si le destructeur dérivé n'est pas invoqué , sauf si la fonction de désallocation sélectionnée est un operator delete destructeur (depuis C++20) .
Une directive utile est que le destructeur de toute classe de base doit être public et virtuel ou protégé et non-virtuel , chaque fois que des expressions delete sont impliquées , par exemple lorsqu'utilisé implicitement dans std::unique_ptr (depuis C++11) .
Pendant la construction et la destruction
Lorsqu'une fonction virtuelle est appelée directement ou indirectement depuis un constructeur ou depuis un destructeur (y compris pendant la construction ou la destruction des membres de données non statiques de la classe, par exemple dans une liste d'initialisation ), et que l'objet auquel l'appel s'applique est l'objet en cours de construction ou de destruction, la fonction appelée est le final overrider dans la classe du constructeur ou du destructeur et non celle qui la remplace dans une classe plus dérivée. En d'autres termes, pendant la construction ou la destruction, les classes plus dérivées n'existent pas.
Lors de la construction d'une classe complexe avec plusieurs branches, dans un constructeur appartenant à une branche, le polymorphisme est restreint à cette classe et ses bases : s'il obtient un pointeur ou une référence vers un sous-objet de base en dehors de cette sous-hiérarchie, et tente d'invoquer un appel de fonction virtuelle (par exemple en utilisant un accès membre explicite), le comportement est indéfini :
struct V { virtual void f(); virtual void g(); }; struct A : virtual V { virtual void f(); // A::f est le remplacement final de V::f dans A }; struct B : virtual V { virtual void g(); // B::g est le remplacement final de V::g dans B B(V*, A*); }; struct D : A, B { virtual void f(); // D::f est le remplacement final de V::f dans D virtual void g(); // D::g est le remplacement final de V::g dans D // note : A est initialisé avant B D() : B((A*) this, this) {} }; // le constructeur de B, appelé depuis le constructeur de D B::B(V* v, A* a) { f(); // appel virtuel à V::f (bien que D ait le remplacement final, D n'existe pas) g(); // appel virtuel à B::g, qui est le remplacement final dans B v->g(); // le type de v est V, base de B, l'appel virtuel appelle B::g comme précédemment a->f(); // le type de a est A, qui n'est pas une base de B. Il appartient à une branche différente de // la hiérarchie. Tenter un appel virtuel via cette branche provoque // un comportement indéfini même si A était déjà entièrement construit dans ce // cas (il a été construit avant B puisqu'il apparaît avant B dans la liste // des bases de D). En pratique, l'appel virtuel à A::f sera // tenté en utilisant la table des fonctions membres virtuelles de B, car c'est celle // qui est active pendant la construction de B) }
Mots-clés
Rapports de défauts
Les rapports de défauts modifiant le comportement suivants ont été appliqués rétroactivement aux normes C++ précédemment publiées.
| DR | Appliqué à | Comportement publié | Comportement correct |
|---|---|---|---|
| CWG 258 | C++98 |
une fonction membre non-const d'une classe dérivée pouvait devenir
virtuelle à cause d'une fonction membre virtuelle const de sa base |
la virtualité nécessite aussi que les qualifications
cv soient identiques |
| CWG 477 | C++98 | une déclaration friend pouvait contenir le virtual spécificateur | non autorisé |
| CWG 1516 | C++98 |
la définition des termes "appel de fonction virtuelle"
et "appel virtuel" n'était pas fournie |
fournie |
Voir aussi
| classes dérivées et modes d'héritage | |
override
spécificateur
(C++11)
|
déclare explicitement qu'une méthode remplace une autre méthode |
final
spécificateur
(C++11)
|
déclare qu'une méthode ne peut pas être remplacée ou qu'une classe ne peut pas être dérivée |