vendredi 19 octobre 2012

Fonctions Virtuelles en C++


Utilité

Nous avons acquis dans le chapitre précédent, la notion d’héritage. Elle nous permet en outre de créer de véritables arbres de classes. Reprenons notre exemple de Point et de PointCol. Nous avons implémenté des méthodes qui permettent l’affichage, ou encore l’initialisation des données, dans chacune des deux classes : Affiche dans PointAfficheCol dans PointCol par exemple.
Je suppose que vous vous êtes demandé pourquoi nous ne leur avons pas donné le même nom ! Le mieux pour le comprendre est d’essayer.
... // Définition de la classe PointCol identique...
void PointCol::Affiche ()
{
 cout << "Point (" << x << ", "<< y << ") ";
 cout << "de couleur : RGB("<<(int)byRed<<","<<(int)byGreen;
 cout << "," << (int)byBlue << ")." << endl;
}
Cette déclaration de la classe PointCol à l’exécution, donne les résultats voulus, à savoir que c’est la "bonne" méthode Affiche qui est appelée. En fait, la liaison est établie statiquement à la compilation. Ici, le compilateur sait très bien quelle fonction membre utiliser. Mais maintenant, imaginons l’utilisation suivante :
void main()
{
 PointCol ptc(5,10, 50,150,200);
 ptc.Affiche();
 Point pt(52,17);
 pt.Affiche();
 Point *ppt; // Pointeur de point "générique"
 ppt = &ptc;
 // le pointeur pointe désormais sur un point coloré : légal ! 
 ppt->Affiche();
}
Le résultat peut vous sembler surprenant. En fait, le typage étant effectué statiquement, pour le compilateur, ppt reste quoi qu’il advienne un pointeur sur Point.
Or, nous n’avions pas encore vu cela, mais il est possible d’affecter une adresse de classe fille à un pointeur de classe de base…[6]
Dans ce cas, vu que l’affectation est dynamique, un appel de la méthode Affiche utilise en fait la déclaration de la classe de base, Point.
Un autre exemple pour illustrer l’utilité des fonctions virtuelles, consisterait à réaliser un affichage "descendant". Il s’agit de réaliser un affichage de toutes les informations relatives aux deux classes, Point et PointCol, en appelant une seule méthode de Point. Vous comprendrez mieux cela, en étudiant le code :
// Point.h 
class Point
{
 int x;
 int y;
public :
 Point(int a, int b){ x=a; y=b; }
 void Init(int a, int b){ x=a; y=b; }
 void Deplace(int a, int b){ x+=a; y+=b; }
 void Affiche();
 void AfficheTout();
};
// Point.cpp 
#include "Point.h"
void Point::Affiche()
{
 cout << this << "->" << x << ", " << y << endl;
}
void Point::AfficheTout()
{
 cout << this << "->" << x << ", " << y << endl;
 Affiche();
}
// PointCol.h
#include "Point.h"
class PointCol : public Point
{
 unsigned char byRed;
 unsigned char byGreen;
 unsigned char byBlue;
public :
 PointCol(int,int,unsigned char,unsigned char,unsigned char);
 void Colore( unsigned charunsigned charunsigned char );
 void Affiche();
};
PointCol::PointCol( int Abs, int Ord, unsigned char R, unsigned char G, unsigned char B) : Point(Abs, Ord)
{
 byRed = R;
 byGreen = G;
 byBlue = B;
}
void PointCol::Colore( unsigned char R, unsigned char G, unsigned char B )
{
 byRed = R;
 byGreen = G;
 byBlue = B;
}
void PointCol::Affiche()
{
 cout << "Couleur : RGB(" << (int)byRed << "," << (int)byGreen;
 cout << "," << (int)byBlue << ")." << endl;
}
En appelant AfficheTout dans le programme, nous souhaitons afficher les renseignements de la classe Point (code contenu dans la première ligne de la méthode), mais aussi ceux de la classe appelante. Par exemple :
void main()
{
 PointCol ptc(5,10, 50,150,200);
 ptc.Affiche();
 Point pt(52,17);
 pt.Affiche();
 ptc.AfficheTout();
}
Dans ce cas précis, nous affichons les informations de couleurs de ptc, puis de coordonnées de pt. La dernière ligne devrait afficher les informations de coordonnées et de couleurs de ptc. Mais dans ce premier test, il n’en est rien !

Mécanisme

Dans le paragraphe précédent, nous avons vu que dans certaines conditions, donner le même nom à une méthode de la classe fille qu’une méthode de la classe de base, peut être source d’erreur, ou plutôt, d’incompréhension.
Pour éviter cela, il faudrait pouvoir indiquer au compilateur de lier certaines méthodes dynamiquement. En effet, il y a des cas où, seulement à l’exécution, le programme peut savoir quelle fonction membre employer. Les fonctions virtuelles servent à cela. Soit la définition suivante :
// Point.h 
class Point
{
 int x;
 int y;
public :
 Point(int a, int b){ x=a; y=b; }
 void Init(int a, int b){ x=a; y=b; }
 void Deplace(int a, int b){ x+=a; y+=b; }
 virtual void Affiche();
 void AfficheTout();
};
// Point.cpp 
void Point::Affiche()
{
 cout << this << "->" << x << ", " << y << endl;
}
void Point::AfficheTout()
{
 cout << this << "->" << x << ", " << y << endl;
 Affiche();
}
Essayez maintenant les deux tests précédemment implémentés. Vous constaterez que grâce à ce virtual, la liaison est désormais dynamique et donc, que la bonne méthode est appelée.

Remarque

Les fonctions virtuelles permettent ainsi de mettre en œuvre un processus très intéressant de liaison dynamique, en ce sens que la bonne méthode à utiliser est sélectionnée à l’exécution. Cette souplesse est couramment utilisée lorsque l’on se retrouve avec de nombreuses classes héritées, et que l’on passe par exemple en paramètre un pointeur sur la classe mère.
Cette facilité a un coût : elle est très gourmande ! En effet, la liaison dynamique est assez lourde, et il convient d’en disposer avec parcimonie. De ce fait, il est parfois fortement conseillé de concevoir son programme sans utiliser ce processus, par exemple au sein d’un traitement d’image, qui est déjà suffisamment gourmand...

Identification de type à l’exécution

Pour information, et sans rentrer dans les détails, il est intéressant de savoir que le C++ a introduit au cours de ses évolutions, la possibilité de connaître le type d’une variable (identification, comparaison), à l’exécution[7]. Cette possibilité fait partie de la fameuse Librairie Standard, que nous n’avons pas encore abordée, et qui fera l’objet du chapitre 10.
Voici un exemple très succinct :
#include <iostream>
using namespace std;
int n(10);
cout << typeid(n).name() << endl;
int nn(15);
if( typeid(n)==typeid(nn)
    cout << "Meme type !" << endl;
La ligne "using namespace std;" permet de faire connaître au compilateur cette possibilité.
En bref, typeid(variable) renvoie un id de type, qui peut être comparé avec l’opérateur classique de comparaison. Il est également possible de connaître le nom du type, avec typeid(variable).name(), ou encore, de savoir si une variable est d’un type ascendant à celui d’une autre, à l’aide de typeid(variable).before(). Vous comprendrez l’intérêt sous-jacent, notamment en polymorphisme.
Ainsi :
#include
class A {
    int n;
};
class B : public A {
    int nn;
};
int main()
{
    A a;
    B b;
    cout << typeid(a).before(typeid(b));
    return 0;
}
Nous obtiendrons à l’affichage "1", puisque la variable a est bien d’une classe mère au type de la variable b.
Pour mieux comprendre le fonctionnement de la librairie standard, se référer à la partie 10.
De plus, il faut savoir que cette possibilité est essentiellement utilisée dans le cadre du développement des programmes, par exemple pour le débogage.

Aucun commentaire:

Enregistrer un commentaire