Hello,

Envoyé par
koala01
La virtualité a beau être une option, elle n'en est pas moins indispensable du point de vue orienté objet dés que l'on parle d'héritage.
Ben justement, moi je ne trouve pas. Au contraire, je trouve que c'est une des forces du C++ de faire clairement le distingo entre les deux et de permettre d'exprimer une approche objet d'un programme en limitant au minimum les contraintes résolues au runtime.
En effet, le fondement même de la conception orientée objets est la substituabilité : le fait que l'on puisse considérer un objet d'un type particulier (par exemple de type Voiture) comme étant du type de la classe de base (par exemple : véhicule) tout en gardant les comportements (du moins, ceux qui existent dans la classe de base)
adaptés au type réellement manipulé.
Pas nécessairement.
Retirer la virtualité du concept orienté objets, reviens à retirer le moteur ou les roues d'une voiture : on obtient quelque chose qui n'est plus en état de fournir les services que l'on en attend de manière correcte
Ben non. Si ton véhicule est réputé avoir des roues, le fait de le spécialiser en voiture n'implique pas forcément que tu vas modifier ses roues. Et étendre la classe ne supprime en rien l'existant.
En outre, il faut être conscient que l'héritage est quand meme la relation la plus forte qui puisse exister entre deux objets car c'est une relation EST-UN, au sens sémantique du terme (on peut décemment dire qu'une voiture EST, du point de vue sémantique, UN véhicule

).
Il faut donc veiller à respecter strictement le
Principe de Substitution de Liskov (
Liskov Subtsitution Principle en anglais, ou
LSP pour les intimes ) et à recourir à l'héritage uniquement quand LSP est strictement respecté.
Eh bien, pour moi, c'est justement le LSP qui va faire que la virtualité doit être forcément optionnelle et, dans ce cas, explicite (même si Java adopte l'approche inverse).
Le LSP est le principe qui impose que toute propriété qui est vraie pour tout objet de type A l'est forcément aussi pour tout objet d'un type B sous-type de A. Le principe de substitution dit clairement qu'on doit pouvoir remplacer un A par un B de façon complètement transparente. C'est particulièrement important lorsque les routines qui reçoivent en arguments des objets d'un type donné n'ont pas connaissance de l'existence de sous-classe de ce type. Et justement, en virtualisant des fonctions-membres, on brise cette transparence.
Ce n'est pas un détail parce que virtualiser des fonctions rend non déterministes des choses qui l'étaient pourtant au départ. Prenons l'exemple classique : une classe « forme » que je dérive en « cercle », « carré », « triangle », etc.
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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
| #include <string>
#include <iostream>
using namespace std;
class Forme
{
public:
virtual string QueSuisJe ();
};
class Cercle : public Forme
{
public:
virtual string QueSuisJe ();
};
class Carre : public Forme
{
public:
virtual string QueSuisJe ();
};
class Triangle : public Forme
{
public:
virtual string QueSuisJe ();
};
string Forme::QueSuisJe ()
{
return "Forme";
}
string Cercle::QueSuisJe ()
{
return "Cercle";
}
string Carre::QueSuisJe ()
{
return "Carré";
}
string Triangle::QueSuisJe ()
{
return "Triangle";
}
int main (void)
{
unsigned int i;
Forme * tableau [4];
Forme fo;
Cercle ce;
Carre ca;
Triangle tr;
tableau[0] = &fo;
tableau[1] = &ce;
tableau[2] = &ca;
tableau[3] = &tr;
for (i=0;i<sizeof tableau / sizeof (*tableau);++i)
cout << "Position " << i << ", je suis un(e) " << tableau[i]->QueSuisJe() << endl;
return 0;
} |
1 2 3 4 5 6
| $ ./programme
Position 0, je suis un(e) Forme
Position 1, je suis un(e) Cercle
Position 2, je suis un(e) Carré
Position 3, je suis un(e) Triangle
$ _ |
On pourrait déjà objecter que c'est le mauvais exemple parce que « Cercle », « Carré » et « Triangle » sont plus restrictives que « Forme » en général qui peut être n'importe quoi, mais c'est une question de définition. Il s'agit de savoir si la classe Forme peut définir à elle seule n'importe quelle forme (avec une liste de points, par exemple), ou si elle ne sert qu'à indiquer qu'il s'agit d'une forme, sans plus de détails. Mais ce n'est pas ce qui nous intéresse.
Maintenant, imaginons que je compte allouer de la place pour stocker les chaînes renvoyées par les objets de mon tableau. Sans virtualité, je sais qu'ils sont tous les mêmes, et en plus je connais cette chaîne à l'avance. J'ai donc tout-à-fait le droit d'écrire par exemple :
ptr = new char[tableau[0]->QueSuisJe().length() * sizeof tableau / (sizeof *tableau)];
Par contre, je ne peux absolument le garantir à l'avance avec des fonctions virtuelles. Ça a même toutes les chances de planter puisque j'ai volontairement choisi le nom « Forme » plutôt que « Polygone » ou autre pour qu'il soit le plus court de tout les noms définis ici et que les autres provoquent systématiquement un dépassement de tableau. :-)
Pire encore : si j'admets ce qui vient d'être dit et que je ne m'en tiens qu'à un seul élément du tableau, si :- Je mesure la longueur de la chaîne renvoyée par l'élément tableau[0] ;
- j'alloue l'espace correspondant ;
- Je trie mon tableau ;
- Je stocke la chaîne que me renvoie mon élément [0] dans l'espace alloué
Alors le programme risquera de planter aussi puisque la chaîne renvoyée ne sera plus la même. Moralité : non seulement je ne peux pas systématiquement substituer une classe virtualisée par une classe dérivée à la compilation de façon déterministe, mais je ne peux pas non plus, même à l'exécution, substituer entre elles les différentes instances d'une même classe virtualisée. le LSP est enterré.
Évidemment, ça ne veut pas dire qu'il faut se passer de virtualité en général : c'est à la fois un principe naturel aux utilisateurs et extrêmement utile en pratique. Mais je trouve qu'il est important de séparer ces notions et de laisser le choix au programmeur.
Ça a également le mérite de mettre en évidence le fait que le LSP peut être, selon les cas, interprété de manières diamétralement opposées.
Partager