Aller au menu - Aller au contenu

Le polymorphisme I


Informations sur le tutoriel

Avatar
Auteur : Nanoc
Difficulté : Difficile
Visualisations : 547 498
Licence : Creative Commons BY-NC-SA


Plus d'informations Plus d'informations

Historique des mises à jour

  • Le 21/02/2010 à 12:41:03
    Correction orthographique
  • Le 17/02/2010 à 10:54:46
    Correction orthographique
  • Le 02/02/2010 à 22:33:37
    Correction lien C++0x -> C++1x
Le polymorphisme... vaste sujet...

Vous en avez peut-être entendu parler sur le forum et beaucoup disent qu'il s'agit d'un sujet difficile. Vous verrez cependant que ce n'est pas vrai ;) et que c'est en réalité quelque chose de très utile pour vos programmes.

Comme il y a beaucoup à dire, j'ai séparé la matière en deux chapitres qui vont ensemble avec un seul QCM à la fin.
Chapitre précédent Sommaire Chapitre suivant

Définition

Petit tour en Grèce



Aujourd'hui n'est pas coutume, nous allons commencer par un petit cours de grec.

Sauriez-vous me dire ce que signifie « polymorphe » ? o_O


Si vous connaissiez vos racines, vous reconnaitriez peut-être les mots « poly » et « morphe » et peut-être vous souvenez-vous de leur signification. « Poly » signifie « plusieurs » comme dans polygone ou polytechnique et « morphe » signifie « forme » comme... euh... polymorphe ou métamorphe. :)


Ok... Mais en C++, ça veut dire quoi ?


Eh bien, la même chose ! Un code (ou bout de code) polymorphe est donc un code qui peut fonctionner de manière différente selon le type qui l'utilise.
D'ailleurs, il existe même quatre sortes de polymorphismes en C++. :'(

Les quatre polymorphismes



Je sens que je vous fais de plus en plus peur. Mais ne partez pas tout de suite, vous allez voir que vous savez déjà bien des choses.

La surcharge de fonctions



Celui-là, vous le connaissez déjà ! Dans votre code, vous pouvez avoir deux fonctions qui ont le même nom :

Code : C++ - Surcharge de fonction
1
2
double puissance(double x,int exposant);
double puissance(double x,double exposant);


Ceci est tout à fait légal puisque le deuxième argument n'est pas le même. Elles n'ont pas la même signature. Selon le type du deuxième argument, ce sera la première ou la deuxième fonction qui sera appelée. C'est donc bien du polymorphisme. On parle parfois de polymorphisme ad hoc.

Les conversions implicites



Celui-là, vous le connaissez aussi. :) Quand vous écrivez une fonction comme ceci :

Code : C++
1
double sqrt(double x);


Vous pouvez également l'utiliser de la manière suivante :

Code : C++
1
2
float a=2.;
a = sqrt(a);


Le float sera automatiquement converti double. La même fonction agit sur un double ou sur un float, c'est donc du polymorphisme. On parle également de polymorphisme ad hoc dans ce cas.

Le polymorphisme paramétrique



Il s'agit des modèles ou template. On y reviendra plus tard. Le but ici est de créer des bouts de codes ou des classes qui peuvent travailler avec n'importe quel type de donnée. On parle aussi de programmation générique.

Le polymorphisme d'inclusion (ou polymorphisme universel)



Il s'agit de ce qui va nous intéresser directement dans la suite. Le code n'agit pas de la même manière pour des objets différents quand ceux-ci sont dans la même hiérarchie (deux classes « soeurs » par exemple). Comment cela ? Nous allons le découvrir.

Il n'est pas nécessaire de connaître ces définitions. Elles sont là pour replacer ce que vous apprenez dans un contexte plus large. Elles sont principalement utiles pour ceux qui tiennent à comparer des langages.


Sachez également qu'un langage de programmation n'est réellement orienté objet que s'il possède le polymorphisme. Les classes et l'héritage peuvent très bien être « simulés » en C (par exemple) qui n'est pas un langage orienté objet. Le polymorphisme est un mécanisme essentiel du paradigme objet.

La résolution des liens

Prenons les classes suivantes comme exemple :

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
using namespace std;

class Forme{
    public:
    void sePresenter() const
    {
        cout << "Je suis une Forme." << endl;
    }
};

class Cercle: public Forme
{
    public:
    void sePresenter() const
    {
       cout << "Je suis un Cercle." << endl;
    }
    private:
    double m_rayon;
};

class Carre: public Forme
{
    public:
    void sePresenter() const
    {
       cout << "Je suis un Carre." << endl;
    }
    private:
    double m_cote;
};
// ...


Vous reconnaîtrez un héritage simple avec un masquage de fonction. Le bout de code suivant ne devrait pas vous poser de problème :

Code : C++
1
2
3
4
Forme a;
a.sePresenter();   // Affiche "Je suis une Forme."
Cercle b;
b.sePresenter();   // Affiche "Je suis un Cercle."


La résolution statique des liens



Jusque là, rien d'inhabituel. Mais que se passe-t-il si on crée une nouvelle fonction recevant en paramètre une Forme :

Code : C++
1
2
3
4
void affichage(Forme x)
{
    x.sePresenter();
}


Et maintenant, écrivons donc un main() qui utilise cette fonction.

Code : C++
1
2
3
4
5
6
7
8
9
int main()
{
    Forme a;
    Cercle b;
    affichage(a);
    affichage(b);

    return 0;
}


Cela ne pose pas de problème puisque un Cercle EST UNE Forme. On peut donc sans problème passer un Cercle en argument à la fonction affichage() qui attend en réalité une Forme.

Le résultat de ce programme sera donc...

Code : Console
Je suis une Forme.
Je suis une Forme.


:( :( On a perdu la vraie nature du Cercle quand on l'a passé à la fonction.

Comment le Cercle s'est-il « changé » en Forme ?


Il faut voir la classe Cercle comme une sorte de poupée russe. La classe Cercle contient d'une certaine manière une instance d'une Forme.

Image utilisateur


Un Cercle EST UNE Forme « améliorée » avec des éléments supplémentaires (l'attribut m_rayon et la nouvelle version de sePresenter()).

La fonction affichage(), elle, attend une Forme. Au moment où l'on passe le Cercle à la fonction, ce dernier perd sa partie « améliorée » et ne garde que sa partie héritée.

Image utilisateur


Et c'est donc la seule fonction sePresenter() qui reste qui est appelée, celle qui affiche « Je suis une forme. ».

On parle dans ce cas de résolution statique des liens. Le mot statique veut dire « connu à la compilation ». Lors de la compilation de la fonction affichage(), on n'a aucun moyen de savoir que plus tard on va lui passer non pas une Forme mais une « Forme améliorée » en l'occurrence un Cercle.

C'est le type de la variable qui détermine quelle fonction membre appeler et pas sa vraie nature.

La résolution dynamique des liens



Ce qu'on aimerait nous, c'est que le Cercle se présente comme tel dans la fonction affichage(). On parle alors de résolution dynamique des liens. Le dynamique signifiant « connu lors de l'exécution ».
Quand on lance le programme, on sait que c'est un Cercle que l'on passe à la fonction affichage() et pas une Forme, il faudrait que le programme en tienne compte. C'est justement ce que nous allons apprendre à faire.

Pour faire cela, il faut deux « ingrédients » :

  • Une référence ou un pointeur sur l'objet.
  • Des fonctions membres virtuelles.


Si vous n'avez pas ces deux choses, vous retombez dans le cas « normal » et le polymorphisme ne se fera pas.

Les fonctions virtuelles

Déclarer une fonction virtuelle



Pour cela, rien de plus simple. Il suffit de mettre le mot-clé virtual devant la fonction.

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;

class Forme{
    public:
    virtual void sePresenter() const
    {
        cout << "Je suis une Forme." << endl;
    }
};

class Cercle: public Forme
{
    public:
    virtual void sePresenter() const
    {
       cout << "Je suis un Cercle." << endl;
    }
    private:
    double m_rayon;
};
// ...


Il n'est pas nécessaire de remettre le virtual dans la classe Fille. La fonction est automatiquement virtuelle par héritage.


Les fonctions static ne peuvent pas être virtuelles ! Les fonctions inline par contre le peuvent sans problème.


Le deuxième ingrédient est un pointeur (ou une référence) sur l'objet. Il nous faut donc récrire la fonction en conséquence.

Code : C++
1
2
3
4
void affichage(const Forme& x)  // On utilise une référence plutôt qu'une copie
{
    x.sePresenter();
}


On peut maintenant réutiliser le même main() que précédemment, ce qui nous donne :

Code : C++
1
2
3
4
5
6
7
8
9
int main()
{
    Forme a;
    Cercle b;
    affichage(a);
    affichage(b);

    return 0;
}


Et cette fois, on a bien le résultat voulu :

Code : Console
Je suis une Forme.
Je suis un Cercle.


Ce n'est donc pas très difficile à utiliser comme concept, mais voyons ce à quoi ça peut servir sur un exemple concret.

Un exemple

Reprenons notre petit RPG comme exemple de l'utilité du polymorphisme.

Attaquer son ennemi



Nous avions trois classes. Personnage ainsi que Magicien et Guerrier qui héritaient de la première.

Image utilisateur


Ce que nous aimerions, c'est ajouter une fonction membre attaquer(const Personnage& autre) qui ait un comportement différent selon que l'on a affaire à un magicien ou à un Guerrier, le premier utilisera une attaque magique et le second sa force brutale.

Rien de plus simple. Ajoutons donc les fonctions nécessaires.

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void Personnage::attaquer(const Personnage& ennemi) const
{
   cout << m_nom << " attaque " << ennemi.getNom() << endl;
}

void Guerrier::attaquer(const Personnage& ennemi) const
{
   cout << m_nom << " tranche violemment le bras de " << ennemi.getNom() << endl;
}

void Magicien::attaquer(const Personnage& ennemi) const
{
   cout << m_nom << " jette un sort à " << ennemi.getNom() << endl;
}


Il faut également ajouter les prototypes dans les fichiers .h et la fonction getNom() dans la classe personnage, mais ça, vous savez le faire.


Une armée de Personnages



Pour mener la guerre, tout le monde sait qu'un soldat seul n'est pas suffisant. Il nous faut donc une armée, autrement dit un groupe de Guerriers et Magiciens. Une solution pourrait par exemple être de faire deux armées séparées, une pour chaque type de soldats.

Code : C++
1
2
3
4
5
#include <vector>
using namespace std;

vector<Guerrier> armeeLourde;
vector<Magicien> armeeMagique;


Ceci fonctionne très bien, mais cette méthode souffre quand même de quelques défauts.

  • Ce code n'est pas très générique. Le jour où l'on voudra ajouter un nouveau type de soldats, il faudra créer un nouveau vector et réaménager tout le code en conséquence.
  • On a créé deux tableaux alors qu'en réalité les deux sortes de soldats sont des Personnages. On pourrait donc en créer un seul.


Si l'on tient compte de ces deux remarques, on serait alors tenté de faire quelque chose de ce type:

Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
#include <vector>
using namespace std;

int main()
{
    vector<Personnage> armee;
    Magicien merlin("Merlin");
    Guerrier lancelot("Lancelot");
    
    armee.push_back(merlin);   // Un Magicien EST UN Personnage.
    armee.push_back(lancelot);
 
    // Jusque-là, tout va pour le mieux. Passons donc à l'attaque...
   
    Personnage goliath("Goliath le tenebreux");

    armee[0].attaquer(goliath);  // Une attaque de magicien.
    armee[1].attaquer(goliath);  // Et un bon coup de hache.

    return 0;
}


Si vous avez bien suivi la théorie, vous devriez être capable de dire ce que donne ce code.

Secret (cliquez pour afficher)
Code : Console
Merlin attaque Goliath le tenebreux
Lancelot attaque Goliath le tenebreux


Eh oui ! En mettant notre Guerrier et notre Magicien dans notre armee, nous avons perdu les informations qui précisaient leur vraie nature. Et c'est là tout l'intérêt du polymorphisme, traiter de manière identique des types différents.

Ajoutons donc les deux ingrédients.

1) Le mot-clé virtual devant le nom de la fonction.

Code : C++ - Personnage.h
1
2
3
4
5
6
class Personnage{
// Reste de la classe

public:
   virtual void attaquer(const Personnage& autre) const;
};


Pas de virtual dans le .cpp ! Il vient seulement et uniquement dans le .h


2) Et les pointeurs sur les différentes instances. Modifions donc le main() pour utiliser des pointeurs.

Ici, on est obligé d'utiliser des pointeurs puisqu'on ne peut mettre des références dans un vector.


Code : C++
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
int main()
{
    vector<Personnage*> armee;   // Notez le caractère « * » ici.
    Magicien merlin("Merlin");
    Guerrier lancelot("Lancelot");
    
    armee.push_back(&merlin);   // Il faut donc passer l'adresse ici.
    armee.push_back(&lancelot);
 
    Personnage goliath("Goliath le tenebreux");

    armee[0]->attaquer(goliath);  // Et utiliser -> plutôt que le . pour accéder aux fonctions.
    armee[1]->attaquer(goliath);

    return 0;
}


On appelle un tableau de ce type une collection hétérogène, puisqu'elle contient différents types de variables.


Cette fois, on obtient ce que l'on veut.

Code : Console
Merlin lance un sort à Goliath le tenebreux
Lancelot tranche le bras de Goliath le tenebreux


C'est là toute la magie du polymorphisme.

Un petit exercice



Pour vous exercer un peu, vous pourriez :
  • Ajouter d'autres fonctions virtuelles aux Personnages.
  • Créer un nouveau type de Personnages, par exemple des Chasseurs qui attaquent à distance avec un arc.
  • Créer une classe Armée qui permettrait de gérer l'ajout de soldats, la suppression de soldats...

Il faut vous souvenir de la règle suivante :

Lorsque une fonction membre virtuelle est appelée à partir d'un pointeur ou d'une référence sur un objet, c'est la fonction membre associée au vrai type de l'instance qui va s'exécuter.

Si vous avez compris cela, vous avez déjà fait un bon bout du chemin.
Chapitre précédent Sommaire Chapitre suivant

Informations sur le tutoriel

Retour en haut Retour en haut

Créé : Le 11/06/2008 à 13:11:39
Modifié : Le 02/02/2010 à 13:22:15
Avancement : 100%

L'orthographe, la grammaire et la présentation de ce tutoriel ont été vérifiées par les zCorrecteurs.
15 commentaires