====== Méthodes virtuelles & Polymorphismes ======
===== Pourquoi les méthodes virtuelles =====
Nous considérons une classe ''Color'' qui fournit deux méthodes, une méthode ''name()'' qui retourne le nom de la couleur et une méthode ''rgbValue()'' retourne les composants RGB de la couleur (Rouge, Vert & Bleue) au format 24 bits.
class Color
{
public:
std::string name() const { return ""; }
unsigned rgbValue24() const { return 0; }
};
Nous souhaitons hériter que les objets ''Blue'', ''Red'' et ''Green'' qui héritent de cette classe redéfinisse les fonctions ''name'' et ''rgbValue24''. L'objectif et que ces fonctions fournissent la bonne couleur.
class Red: public Color
{
public:
std::string name() const { return "red"; }
unsigned rgbValue24() const { return 0xFF0000; }
};
class Green: public Color
{
public:
std::string name() const { return "green"; }
unsigned rgbValue24() const { return 0x00FF00; }
};
class Blue: public Color
{
public:
std::string name() const { return "blue"; }
unsigned rgbValue24() const { return 0x0000FF; }
};
Nous souhaitons désormais pouvoir utiliser ces couleurs pour remplir une surface:
void fillrect(const Color& theColor, int x, int y, int width, int height)
{
std::cout << "Fill the area [" << x << ", " << y << ", " << x + width << ", " << y + height
<< "] with color " << theColor.name() << "." << std::endl;
}
Cependant, si nous exécutons le code suivant :
fillreact(Red(), 0, 0, 10, 10);
La sortie sera:
Fill the area [0, 0, 10, 10] with color .
Ce qui est parfaitement normal puisque lors de l'appel de la fonction ''name()'' :
std::cout << "Fill the area [" << x << ", " << y << ", " << x + width << ", " << y + height
<< "] with color " << theColor.name() << "." << std::endl;
le type de ''theColor'' est ''Color''. Donc le compilateur va appelé la méthode ''Color::name()'' et non pas la méthode ''Red::name()'' bien que l'objet a pour type effectif ''Red''.
Nous voyons une différence entre deux notions de type:
* le type réel de l'objet (celui lors de la création) qui est ''Red'' dans le cas précédent;
* le type de manipulation de l'objet (celui au moment ou l'on appelle la fonction) qui est ''Color'' dans le cas précédent.
Par défaut, la méthode appellée est celle qui est définie dans le type de manipulation et non pas le type de l'objet lors de la création. Cependant, cela n'est pas satisfaisant. Nous aimerions pouvoir remplacer dans le type ''Color'' la méthode ''name()'' par la méthode ''name()'' définie dans le type ''Red''.
===== Définir une méthode virtuelle =====
En reprenant l'exemple précédent, nous pouvons introduire le mot clé ''virtual'' devant chacune des deux méthodes qui ont été définies :
class Color
{
public:
virtual std::string name() const { return ""; }
virtual unsigned rgbValue24() const { return 0; }
};
Le fait de mettre le mot-clé ''virtual'' devant chacune des méthodes indique que ces méthodes sont substituables. Ceci signifie que désormais l'exécution de :
fillreact(Red(), 0, 0, 10, 10);
produira la sortie:
Fill the area [0, 0, 10, 10] with color Red.
===== Qu'est ce qui se produit lorsque l'on définit une méthode virtuelle =====
Ajouter la mot clé ''virtual'' indique au compilateur que la méthode qui doit être appelée est la ** méthode qui est définie par le type effectif (celui lors de la création) ** de l'objet.
Ainsi dans la fonction, nous avons l'appel ''theColor.name()'' :
void fillrect(const Color& theColor, int x, int y, int width, int height)
{
std::cout << "Fill the area [" << x << ", " << y << ", " << x + width << ", " << y + height
<< "] with color " << theColor.name() << "." << std::endl;
}
Normalement, sans le mot clé ''virtual'', le compilateur aurait fait les opérations suivantes :
- quel est le type de l'objet ''theColor''. C'est ''Color''.
- on appelle alors la méthode ''Color::name()''.
Avec le mot clé ''virtual'', la compilateur fait les opérations suivantes :
- quel est type de l'objet ''theColor'' **au moment de sa création**. C'est ''Red''.
- on appelle alors la méthode ''Red::name()''.
===== Qu'est ce qui se passe quand on active la méthode virtuelle =====
Quand on ajouter une méthode virtuelle à une classe, on ajoute le stockage d'informations complémentaires. Ces informations complémentaires sont :
- les informations sur le type de l'objet au moment de la création.
- une table des méthodes virtuelles qui permet de savoir que pour la méthode ''name()'', on appelle la méthode ''Red::name()'' et non pas la méthode ''Color::name()''.
Comment s'effectue donc l'appel d'une méthode ''method()'' en C++ :
* Un objet ''object'' de type ''Object'' ne contient aucune méthode virtuelle. Alors on appelle la méthode ''Object::method()''.
* Un objet ''object'' de type ''VirtualObject'' contient une ou plusieurs méthodes virtuelles. On regarde si la méthode est une méthode virtuelle.
* La méthode n'est pas une méthode virtuel, alors on appelle la méthode ''VirtualObject::method()''.
* La méthode est une méthode virtuelle, alors on détermine le type de l'objet au moment de la création. Supposons que l'objet est de type ''DerivedObject'', dans ce cas, le compilateur recherche dans la table des méthodes virtuelles la méthode qui s'appelle ''method()'' à appeller. Supposons que la méthode ''method()'' a été redéfinie dans la classe ''DerivedObject'', alors le compilateur va trouver dans la table des méthodes virtuelles la méthode pour l'entrée ''method()'', la méthode ''DerivedObject::method()''. Si la méthode n'a pas été redéfinie, alors le compilateur va trouver dans la table des méthodes virtuelles pour l'entrée ''method()'', la méthode ''Object::method()''.
===== Définir une méthode virtuelle purement abstaite =====
Regardons les méthodes de la classe ''Color'' :
class Color
{
public:
std::string name() const { return ""; }
unsigned rgbValue24() const { return 0; }
};
Les méthodes ''name()'' et ''rgbValue24()'' retourne des valeurs par défaut. Ce qui n'est d'ailleurs pas souhaitable, puisque quelque part, ces valeurs n'ont pas de sens. Il est possible de définir une classe squelette, appellée aussi classe asbtraite dans laquelle les méthodes sont déclarées mais ne sont pas implantées. Pour la classe ''Color'', cela revient à l'écriture suivante :
class Color
{
public:
std::string name() const = 0;
unsigned rgbValue24() const = 0;
};
La classe ''Color'' expose deux méthodes ''name()'' et ''rgbValue()'' qui sont deux méthodes virtuelles mais n'ayant aucun code. Cela impose aux classes qui héritent de la classe ''Color'' de devoir implanter les méthodes virtuelles.
De plus, une classe qui possède des méthodes virtuelles non définies (purement abstraite) ne peut pas être crée. En conséquence :
Color basicColor;
génère une erreur à la compilation. En effet, vous essayez de créer une classe dont tous les éléments (ici en l'espèce ce sont les méthodes) ne sont pas définis. Ce qui est impossible. Le compilateur vous protège contre ce type d'erreur.