Méthodes virtuelles

Revenons à notre exemple rectangle et rectplein pour mettre en lumière un problème inhérent à l’héritage. Nous ne connaissons pas le contenu de la classe rectangle, mais imaginons qu’il s’agisse simplement des quatre coordonnées h, b, g, d du rectangle. Dans ce cas, la méthode change a probablement l’allure suivante :

void rectangle::change(int gche, int haut,                     int drte, int bas){     efface();     if ( (gche >= drte) || (haut >= bas) )         { g = d; return; }    // rectangle vide     g = gche; d = drte;     h = haut; b = bas;     trace();}

A priori, il n’y a aucune raison de changer cette méthode pour notre classe rectplein. Pourtant, si l’on fait un essai, un appel de change avec une instance de rectplein ne donnera pas le bon résultat. Pourquoi ?

Rappelons-nous que la classe rectangle a été compilée avant la classe rectplein. Dès lors, lorsque le compilateur, agissant sur le code source de la méthode change ci-dessus, rencontre un appel à efface et un autre à trace, il cherche les méthodes de ce nom ; il n’en connaît alors que deux, celles de la classe rectangle. En mettant les points sur les i, le compilateur « voit » donc ceci :

void rectangle::change(//...){     rectangle::efface();     // ...     rectangle::trace();}

On voit dès lors pourquoi cette méthode ne fonctionnera pas correctement avec rectplein : le rectangle ne sera ni correctement effacé, ni correctement retracé, parce que l’on a modifié les méthodes correspondantes dans notre nouvelle classe.

Une solution simple et expéditive consisterait alors à réécrire la méthode change ; seulement voilà, on ne peut pas : nous ne savons pas comment les coordonnées du rectangle sont stockées dans la classe rectangle, et le saurions-nous, nous ne pourrions pas les modifier puisque les membres correspondants sont inaccessibles dans la classe rectplein.

Ce problème est classique en programmation orientée objet, et résulte du principe même de compilation. Les langages de POO interprétés comme SmallTalk n’ont pas de difficultés de cet ordre.

La solution réside dans la déclaration de méthodes virtuelles. Pour les déclarer telles, il suffit de placer le mot réservé virtual devant le nom de la méthode. Si le programmeur qui a conçu la classe rectangle était prévoyant, il a compris que certaines des fonctions membres de la classe auraient à être modifiées dans des classes descendantes :

class rectangle {     // membres privés     public :     rectangle();     rectangle(int gche, int haut, int drte, int bas);     virtual ~rectangle();     virtual void trace();     virtual void efface();     void valeur(int& gche, int&  haut,                 int& drte, int& bas);     void change(int gche, int haut,                 int drte, int bas);     };

Lorsque le compilateur sait qu’une méthode est virtuelle, il ne place pas un appel direct à cette méthode, mais recherche la dernière méthode redéfinie dans la classe de this ayant le même nom et les mêmes types d’arguments, et appelle celle-ci. Il en résulte que la méthode change aura cette fois-ci le bon comportement : si l’on appelle r.changer est de type rectangle, la méthode appellera les fonctions rectangle::efface et rectangle::trace ; si l’on appelle rp.change, où rp est de type rectplein, la méthode appellera cette fois rectplein::efface et rectplein::trace.

On note que les méthodes change et valeur n’ont pas besoin d’être définies comme virtuelles, parce que leur code n’a aucune raison d’être modifié par les classes dérivées de rectangle (en fait, elles ne peuvent pas le modifier, puisqu’elles n’ont pas accès aux coordonnées du rectangle).

Lorsqu’une méthode est déclarée virtuelle dans une classe, elle l’est automatiquement pour toutes les classes dérivées ; il n’est donc pas nécessaire de réécrire virtual devant leur déclaration.

On prendra garde que les méthodes ne sont pas seulement caractérisées par leur nom, mais aussi par la liste de leurs arguments. En conséquence, si l’on définit par exemple une méthode rectplein::trace(int couleur), celle-ci ne sera pas virtuelle (sauf déclaration explicite) car il s’agit d’une méthode différente de rectplein::trace() (en vertu des règles de recouvrement de fonctions vues au chapitre 5) ; de toute façon, ce ne sera pas elle qui sera appelée par rectangle::change, ne serait-ce que parce que les arguments ne correspondent pas. Précisons qu’en parlant de la liste des arguments, nous parlons aussi des arguments par défaut : en définissant une unique méthode rectplein::trace(int couleur = -1) par exemple, on s’expose à des ennuis car la méthode change appellera alors rectangle::trace(), seule méthode de ce nom ayant zéro argument dans la classe rectplein.

Pour éviter tout ennui, on redéfinira une méthode virtuelle avec exactement la même liste d’arguments, quitte à fournir aussi des homonymes ayant des arguments supplémentaires (ou en moins). On évitera aussi les arguments par défaut dans les méthodes virtuelles, pour la même raison.

Précédent Précédent Sommaire Sommaire Suivant Suivant