Table of Contents

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:

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 :

  1. quel est le type de l'objet theColor. C'est Color.
  2. on appelle alors la méthode Color::name().

Avec le mot clé virtual, la compilateur fait les opérations suivantes :

  1. quel est type de l'objet theColor au moment de sa création. C'est Red.
  2. 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 :

  1. les informations sur le type de l'objet au moment de la création.
  2. 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++ :

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.