IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
logo
Sommaire > Les fonctions > Les fonctions membres virtuelles
        Que signifie le mot-clé virtual ?
        Pouvez-vous me donner une raison simple pour laquelle la virtualité est si importante ?
        Les fonctions virtuelles sont-elles un mécanisme important en C++ ?
        Qu'est-ce qu'une fonction virtuelle pure ?
        Qu'est-ce qu'un type de retour covariant ?
        Puis-je appeler des fonctions virtuelles dans le constructeur (ou le destructeur) ?



Que signifie le mot-clé virtual ?
Mise à jour le 22/10/2004[haut]
auteurs : Anomaly, Aurélien Regat-Barrel, Luc Hermitte
Le mot-clé virtual permet de supplanter une fonction membre d'une classe parent depuis une classe dérivée (à condition qu'elle ait la même signature).
class A
{
public:
    void F1() { cout << "A::F1()\n"; }    
    virtual void F2() { cout << "A::F2()\n"; }
};

class B : public A
{
public:
    void F1() { cout << "B::F1()\n"; }    
    void F2() { cout << "B::F2()\n"; }
};

int main()
{
    A a;
    a.F1(); // affiche "A::F1()"
    a.F2(); // affiche "A::F2()"

    B b;
    b.F1(); // affiche "B::F1()"
    b.F2(); // affiche "B::F2()"

    // copie non polymorphe
    a = b;
    a.F1(); // affiche "A::F1()"
    a.F2(); // affiche "A::F2()"

    // utilisation polymorphe de B (par pointeur)
    A * pa = &b;
    pa->F1(); // affiche "A::F1()"
    pa->F2(); // affiche "B::F2()" <-- grace à virtual

    // utilisation polymorphe de B (par référence)
    A & ra = b;
    ra.F1(); // affiche "A::F1()"
    ra.F2(); // affiche "B::F2()" <-- grace à virtual
}
Dans cet exemple, F1() est redéfinie statiquement par B, c'est-à-dire que le compilateur utilise le type officiel de la variable pour savoir quelle fonction appeler. Ainsi, si on appelle F1() depuis un objet de type A, ce sera toujours A::F1() qui sera appelé, et si l'objet est de type B ce sera B::F1() indépendamment du fait qu'il peut s'agir d'un pointeur ou d'une référence sur A qui désigne en réalité un objet de type B (cas d'utilisation polymorphe de B).
L'appel à une fonction membre virtuelle n'est au contraire pas déterminé à la compilation mais lors de l'exécution. Le fait que A::F2() soit déclarée virtual et supplantée par B::F2() signifie qu'à chaque appel de F2() le compilateur va tester le type réel de l'objet afin d'appeler B::F2() s'il le peut. Sinon il appellera A::F2(). On parle alors de liaison dynamique (dynamic binding en anglais) par opposition à la liaison statique faite lors de l'édition de liens.
La virtualité implique l'utilisation de pointeurs ou de références. Ceci est illustré par le 3° exemple du code ci-dessus qui effectue une recopie non polymorphe d'un objet B vers un objet A. Dans ce cas l'objet B est "tronqué" (pour éviter ce problème il faut passer par une copie polymorphe, voir Comment effectuer la copie d'objets polymorphes ?) et on obtient un objet de type A malgré que l'on soit parti d'un objet de type B.
Ce n'est pas le cas avec l'utilisation de pointeurs ou références, qui bien que déclarés comme étant des pointeurs / références sur des objets de types A peuvent désigner des objets de type B comme dans les deux derniers exemples du code précédent.
Le type statique de pa est A* mais son type dynamique est B*. De même, le type dynamique de ra est B, ce qui explique que pa->F2() et ra.F2() provoquent l'appel de B::F2() alors que statiquement c'est A::F2() qui aurait du être appelé.
Notez que cet exemple n'inclut pas de destructeur virtuel par souci de simplification, mais ceci serait nécessaire. Pour plus d'explications, lire la question Pourquoi et quand faut-il créer un destructeur virtuel ?.


Pouvez-vous me donner une raison simple pour laquelle la virtualité est si importante ?
Créé le 10/02/2004[haut]
auteur : Marshall Cline
L'appel dynamique permet d'augmenter la réutilisabilité en autorisant le 'vieux' code à appeler du nouveau code.

Avant l'apparition de l'orientation objet, la réutilisation du code se faisait en appelant du vieux code à partir du nouveau code. Par exemple, un programmeur peut écrire du code appelant du code réutilisable comme printf(), ....

Avec l'orientation objet, la réutilisation peut aussi être accomplie via l'appel de nouveau code par de l'ancien. Par exemple, un programmeur peut écrire du code qui est appelé par un framework qui a été écrit par son arrière grand-père. Il n'y a pas besoin de modifier le code écrit par l'arrière grand-père. En fait, il n'a même pas besoin d'être recompilé. Et si jamais il ne restait que le fichier objet, et que le code écrit par l'arrière grand-père ait été perdu depuis 25 ans, cet ancien fichier objet appellera le code avec les nouvelles fonctionnalités sans rien changer d'autre.

C'est cela l'extensibilité, et c'est cela l'orientation objet.


Les fonctions virtuelles sont-elles un mécanisme important en C++ ?
Créé le 10/02/2004[haut]
auteur : Marshall Cline
OUI

Sans les fonctions virtuelles, le C++ ne serait pas un langage orienté objet. La surcharge d'opérateur et les fonctions membres non virtuelles sont très pratiques, mais ne sont, finalement qu'une variante syntaxique de la notion beaucoup plus classique de passage de pointeur sur une structure à une fonction. La bibliothèque standard contient de nombreux templates illustrant les techniques de "programmation générique", qui sont aussi très pratiques, mais les fonctions virtuelles sont le coeur même de la programmation orientée objet.

D'un point de vue 'business', il y a très peu de raison de passer du C pur au C++ sans les fonctions virtuelles (pour le moment, nous ferons abstraction de la programmation générique et de la bibliothèque standard). Les spécialistes pensent souvent qu'il a une grande différence entre le C et le C++ non orienté objet ; mais sans l'orientation objet, la différence n'est pas suffisante pour justifier le coût de formation des développeurs, des nouveaux outils, ....
En d'autres termes, si je devais conseiller un gestionnaire concernant le passage du C au C++ sans orientation objet (c'est-à-dire changer le langage sans changer de paradigme), je le découragerais probablement, à moins qu'il y ait des contraintes liées aux outils utilisés. D'un point de vue gestion, la programmation orientée objet permet de concevoir des systèmes extensibles et adaptables, mais la syntaxe seule sans l'orientation objet ne réduira jamais le coût de maintenance, mais augmentera probablement les coûts de formation de façon significative.

Nota : le C++ sans virtualité n'est pas orienté objet. Programmer avec des classes mais sans liaison dynamique est une programmation basée sur des objets, mais n'est pas de la programmation objet. Ignorer la virtualité est équivalent à ignorer l'orientation objet. Tout ce qui reste est une programmation basée sur des objets, tout comme la version originale d'ADA. (Soit dit en passant, le nouvel ADA supporte la véritable orientation objet, et non plus simplement la programmation basée sur les objets).


Qu'est-ce qu'une fonction virtuelle pure ?
Mise à jour le 22/11/2004[haut]
auteurs : LFE, Aurélien Regat-Barrel, Luc Hermitte
Syntaxiquement, une fonction virtuelle pure est une fonction virtuelle suivie de "= 0" dans sa déclaration :
class Test
{
public:
    virtual void F() = 0;  // = 0 signifie "virtuelle pure"
};
Une fonction virtuelle signifie qu'elle peut être supplantée par une fonction d'une classe fille.
Une fonction virtuelle pure signifie qu'elle doit être supplantée par une fonction d'une classe fille.
La classe qui déclare une fonction virtuelle pure n'est alors pas instanciable car elle possède au moins une fonction qui doit être supplantée. On dit alors que c'est une classe abstraite (lire Qu'est-ce qu'une classe abstraite ?).
Notez que virtuelle pure veut simplement dire que la fonction doit être supplantée par les classes filles que l'on veut instanciables, et non que la fonction n'est pas implémentée. Le C++ autorise une fonction virtuelle pure à disposer d'un corps. Une telle pratique sert généralement à forcer une classe à être abstraite (en rendant son destructeur virtuel pur) ou à proposer une implémentation type pour la fonction virtuelle pure.
class A
{
public:
    virtual void f() = 0; // virtuelle pure
};

void A::f()
{
    // implémentation par défaut
}

// B se contente de l'implémentation par défaut de f()
class B : public A
{
public:
    void f()
    {
        A::f();
    }
};

Qu'est-ce qu'un type de retour covariant ?
Créé le 17/10/2005[haut]
auteur : Aurélien Regat-Barrel
Lors de la réimplémentation d'une fonction membre virtuelle dans une classe dérivée, il est possible de ne pas tout à fait respecter le prototype de la fonction virtuelle en renvoyant un type dérivé du type originel :

class A {};
class B : public A {};

class Base
{
public:
    virtual A* Test()
    {
        cout << "Base::Test\n";
        return new A;
    }
};

class Derived : public Base
{
public:
    virtual B* Test() // le type de retour est différent de Base::Test
    {
        cout << "Derived::Test\n";
        return new B;
    }
};

int main()
{   
    Base *b = new Derived;
    A *a = b->Test();
}
Si votre compilateur supporte les retours covariants, alors le message "Derived::Test" devrait s'afficher, et a devrait pointer vers une instance de B. Si "Base::Test" s'affiche, c'est que le compilateur a considéré que le prototype de Derived::Test ne correspondait pas à celui de Base::Test et qu'il s'agissait donc d'une nouvelle fonction membre propre à Derived et non pas d'une réimplémentation de Base::Test (voir Qu'est-ce que le masquage de fonction ?). Dans ce cas, ou en cas de refus de compilation, votre compilateur ne supporte pas les retours covariants (cas de VC++ 6).

Cette possibilité est en particulier utilisée dans le clonage de classes polymorphes. Les retours covariants permettent en effet de transformer le code suivant :

class Clonable
{
public:
    virtual Clonable* Clone() const = 0;
};

class A : public Clonable
{
public:
    virtual Clonable* Clone() const
    {
        return new A( *this );
    }
};

int main()
{
    A *a1 = new A;
    // faire une copie
    A *a2 = static_cast<A*>( a1->Clone() ); // cast obligatoire !
}
En une version plus élégante sans cast :

class Clonable
{
public:
    virtual Clonable* Clone() const = 0;
};

class A : public Clonable
{
public:
    virtual A* Clone() const // retour covariant
    {
        return new A( *this );
    }
};

int main()
{
    A *a1 = new A;
    // faire une copie
    A *a2 = a1->Clone(); // plus de cast
}
Pour plus de détails à ce sujet, lire Comment effectuer la copie d'objets polymorphes ?.


Puis-je appeler des fonctions virtuelles dans le constructeur (ou le destructeur) ?
Créé le 15/10/2009[haut]
auteurs : JolyLoic, Laurent Gomila
Oui, c'est possible, mais attention, ça ne fait pas toujours ce qu'on pense.
La première approche consiste à comprendre que lors de l'appel du constructeur d'une classe de base, la classe dérivée n'a pas encore été construite. Donc, c'est la méthode spécialisée à ce niveau qui est appelée.
Voyons en détail :
La règle est que le type dynamique d'une variable en cours de construction est celui du constructeur qui est en train d'être exécuté. Pour bien comprendre ce qui se passe, il faut donc revenir sur la différence entre le type statique d'une variable, et son type dynamique.

Prenons par exemple trois classes, C qui dérive de B qui dérive de A. Par exemple, dans :
A* a = new B();
La variable a possède comme type statique (son type déclaré dans le programme) A*. Par contre, son type dynamique est B*. Une fonction virtuelle est simplement une fonction dont on va chercher le code en utilisant le type dynamique de la variable, au lieu de son type statique, comme une fonction classique.

Maintenant, quand on crée un objet de type C, les choses se passent ainsi :

  • On alloue assez de mémoire pour un objet de la taille de C.
  • On initialise la sous partie correspondant à A de l'objet.
  • On appelle le corps du constructeur de A. Pendant cet appel, l'objet crée a pour type dynamique A.
  • On initialise la sous partie correspondant à B de l'objet.
  • On appelle le corps du constructeur de B. Pendant cet appel, l'objet crée a pour type dynamique B.
  • On initialise la sous partie correspondant à C de l'objet.
  • On appelle le corps du constructeur de C. Pendant cet appel, l'objet crée a pour type dynamique C.
Donc, dans le corps du constructeur de la classe B, un appel d'une fonction virtuelle appellera la version de la fonction définie dans la classe B (ou à défaut celle définie dans A si la fonction n'a pas été définie dans B), et non pas celle définie dans la classe C.

D'ailleurs, si la fonction est virtuelle pure dans B, ça causera quelques problèmes, puisqu'on tentera alors d'appeler une fonction qui n'existe pas. En général, le programme va planter, si on a de la chance, il affichera une message du style "Pure function called".

La problèmatique est exactement la même pour les destructeurs, mais dans l'ordre inverse.

Pourquoi cette règle ? Une fonction définie dans C a accès aux données membre de C. Or, on a vu que au moment où on exécute l'appel au corps du constructeur de B, ces dernières ne sont pas encore créées. On a donc préféré jouer la sécurité.

lien : faq Dans quel ordre sont construits les différents composants d'une classe ?


Consultez les autres F.A.Q.


Valid XHTML 1.0 TransitionalValid CSS!

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2008 Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.