[20] Héritage - fonctions virtuelles

(Une partie de C++ FAQ Lite fr, Copyright © 1991-2002, Marshall Cline, cline@parashift.com)

Traduit de l'anglais par Stéphane Bailliez

Les FAQs de la section [20]


[20.1] Qu'est ce qu'une "fonction membre virtuelle" ou "méthode virtuelle"?
D'un point de vue OO, il s'agit de la plus importante fonctionnalité du C++: [6.8], [6.9].

Une fonction virtuelle permet aux classes dérivées de remplacer l'implémentation fournie par la classe de base. Le compilateur s'assure que la méthode remplacée est bien appelée quand l'objet en question est bien du type de la classe dérivée, même si l'objet est accédé par un pointeur de base plutôt qu'un pointeur dérivé. Cela permet aux algorithmes de la classe de base d'être remplacé dans la classe dérivée, meme si les utilisateurs ne connaissent pas la classe dérivée.

La classe dérivée peut complétement remplacer ("override") les fonctions membres de la classe de base, ou la classe derivée peut partiellement remplacer ("augment") les fonctions membres de la classe de base. Cette dernière action, si elle est désirée, est obtenue lorsque les fonctions membres de la classe dérivée appellent les fonctions membres de la classe de base.

[ Haut | Bas | Rechercher ]


[20.2] Comment le C++ réalise t-il le lien dynamique(dynamic binding) et le typage statique (static typing)?
[Recently added the definition of polymorphism thanks to Kemberli Jennings (on 1/00).]

Lorsque vous avez un pointeur sur un objet, l'objet peut trés bien être une classe qui est dérivée de la classe du pointeur (e.g. un Vehicle* qui pointe sur un objet Car; cela est appelé le "polymorphisme"). Il y a donc deux types: le type (statique) du pointeur (ici Vehicle), et le type (dynamique) de l'objet qui est pointé (ici Car).

Le typage statique (static typing) signifie que la légalité de l'appel d'une fonction membre est vérifiée le plus tôt possible: lors de la compilation. Le compilateur utilise le type statique du pointeur pour déterminer si l'appel de la fonction membre est légal ou non. Si le type du pointeur permet de peut gérer la fonction membre, alors l'objet pointé peut certainement le gérer également. E.g. si Vehicle a un certain nombre de fonctions membres, alors Car a également ces fonctionnalités car c'est une sorte-de Vehicle.

le lien dynamique (dynamic binding) signifie que l'adresse du code lors de l'appel d'une fonction membre est déterminé au dernier moment possible: basé sur le type dynamic de l'objet lors de l'exécution. On appelle cela le lien dynamique (dynamic binding) car le lien au code qui est appelé est accompli dynamiquement (lors de l'exécution). Le lien dynamique est un résultat des fonctions virtuelles.

[ Haut | Bas | Rechercher ]


[20.3] Quelle est la différence lors de l'appel des fonctions membres virtuelles et non-virtuelles?
Les fonctions membres non-virtualsont résolues statiquement. C'est à dire que la fonction membre est sélectionnée statiquement (durant la compilation) suivant le type du pointeur (ou de la référence) sur l'objet.

Par contraste, les fonctions membres virtual sont résolues dynamiquement (durant l'exécution). C'est à dire que la fonction membre est sélectionnée dynamiquement (durant l'exécution) suivant le type de l'objet et non suivant le type du pointeur ou de la référence sur cet objet. On appelle cela le "dynamic binding". La plupart des compilateurs utilise une variante de la technique suivante: si l'objet a une ou plusieurs fonctions virtual, le compilateur rajoute un pointeur caché dans l'objet appelé "virtual pointer" ou "v-pointer". Ce "v-pointer" pointe sur un table globale appelée "table virtuelle" (virtual table) ou "v-table".

Le compilateur crée une v-table pour chaque classe ayant au moins une fonction virtual. Par exemple si la classe Circle a des fonctions virtual pour draw(), move() et resize()il y aura exactement une v-table associée à la classe Circle, cela quelque soit le nombre d'objets Circle, et le v-pointer de ces objets Circle pointera sur la v-table de Circle. La v-table elle-même à des pointeurs sur chaque fonction virtuelles de la classe. Par exemple, la v-table de Circle aura trois pointeurs: un pointeur sur Circle::draw(), un pointeur sur Circle::move(), et un pointeur sur Circle::resize().

Pendant le dispatch d'une fonction virtuelle, le système récupère la v-table de la classe pointée par le v-pointer de l'objet et récupère ensuite l'emplacement de la méthode donnée par la v-table.

L'overhead taille de la technique ci dessus est nominal: un pointeur additionnel par objet (mais seulement pour les objets qui ont besoin du dynamic binding), ainsi qu'un pointeur par méthode virtuelle. L'overhead performance est également nominal comparé à l'appel d'un fonction normale, une fonction virtual nécessite deux appels supplémentaires (un pour obtenir la valeur du v-pointer, un deuxième pour obtenir l'adresse de la méthode). Aucun de ces overheads n'existe pour les fonctions non-virtual car le compilateur fait la résolution des fonctions non-virtual à la compilation basé sur le type du pointeur.

Note: La discussion ci-dessus est considérablement simplifiée, car il n'est pas tenu compte de la structure obtenue par l'héritage multiple, l'héritage virtuel, le RTTI, etc., ni ne tient compte des pages faults, des appels de fonctions par un pointeur sur fonction, etc. Si vous souhaitez davantage d'information envoyez un message sur comp.lang.c++; S'IL VOUS PLAIT NE M'ENVOYEZ PAS DE MAILS !

[ Haut | Bas | Rechercher ]


[20.4] I have a heterogeneous list of objects, and my code needs to do class-specific things to the objects. Seems like this ought to use dynamic binding but can't figure it out. What should I do?

It's surprisingly easy.

Suppose there is a base class Vehicle with derived classes Car and "Truck". The code traverses a list of Vehicle objects and does different things depending on the type of Vehicle. For example it might weigh the "Truck" objects (to make sure they're not carrying too heavy of a load) but it might do something different with a Car object — check the registration, for example.

The initial solution for this, at least with most people, is to use an if statement. E.g., "if the object is a "Truck", do this, else if it is a Car, do that, else do a third thing":

typedef std::vector<Vehicle*>  VehicleList;

void myCode(VehicleList& v)
{
	for (VehicleList::iterator p = v.begin(); p != v.end(); ++p) {
		Vehicle& v = **p;  // just for shorthand

		// generic code that works for any vehicle...
		...

		// perform the "foo-bar" operation.
		// note: the details of the "foo-bar" operation depend
		// on whether we're working with a car or a truck.
		if (v is a Car) {
			// car-specific code that does "foo-bar" on car v
			...
		} else if (v is a Truck) {
			// truck-specific code that does "foo-bar" on truck v
			...
		} else {
			// semi-generic code that does "foo-bar" on something else
			...
		}

		// generic code that works for any vehicle...
		...
	}
}

The problem with this is what I call "else-if-heimer's disease" (say it fast and you'll understand). The above code gives you else-if-heimer's disease because eventually you'll forget to add an else if when you add a new derived class, and you'll probably have a bug that won't be detected until run-time, or worse, when the product is in the field.

The solution is to use dynamic binding rather than dynamic typing. Instead of having (what I call) the live-code dead-data metaphor (where the code is alive and the car/truck objects are relatively dead), we move the code into the data. This is a slight variation of Bertrand Meyer's Inversion Principle.

The idea is simple: use the description of the code within the {...} blocks of each if (in this case it is "the foo-bar operation"; obviously your name will be different). Just pick up this descriptive name and use it as the name of a new virtual member function in the base class (in this case we'll add a fooBar() member function to class Vehicle).

 class Vehicle {
 public:
   // performs the "foo-bar" operation
   virtual void fooBar() = 0;
 };
Then you remove the whole if...else if... block and replace it with a simple call to this virtual function:
typedef std::vector<Vehicle*>  VehicleList;

void myCode(VehicleList& v)
{
	for (VehicleList::iterator p = v.begin(); p != v.end(); ++p) {
		Vehicle& v = **p;  // just for shorthand

		// generic code that works for any vehicle...
		...

		// perform the "foo-bar" operation.
		v.fooBar();

		// generic code that works for any vehicle...
		...
	}
}
Finally you move the code that used to be in the {...} block of each if into the fooBar() member function of the appropriate derived class:
class Car : public Vehicle {
public:
	virtual void fooBar();
};

void Car::fooBar()
{
	// car-specific code that does "foo-bar" on 'this'
	... this is the code that was in {...} of if (v is a Car)
}

class Truck : public Vehicle {
public:
	virtual void fooBar();
};

void Truck::fooBar()
{
	// truck-specific code that does "foo-bar" on 'this'
	... this is the code that was in {...} of if (v is a Truck)
}
If you actually have an else block in the original myCode() function (see above for the "semi-generic code that does the 'foo-bar' operation on something other than a Car or Truck"), change Vehicle's fooBar() from pure virtual to plain virtual and move the code into that member function:
class Vehicle {
public:
	// performs the "foo-bar" operation
	virtual void fooBar();
};

void Vehicle::fooBar()
{
	// semi-generic code that does "foo-bar" on something else
	... this is the code that was in {...} of the else
	// you can think of this as "default" code...
}

That's it!

The point, of course, is that we try to avoid decision logic with decisions based on the kind-of derived class you're dealing with. In other words, you're trying to avoid if the object is a car do xyz, else if it's a truck do pqr, etc., because that leads to else-if-heimer's disease.

[ Haut | Bas | Rechercher ]


[20.5] Quand le destructeur doit il être virtuel?
Lorsque que vous êtes susceptible de faire un deleted'un objet dérivé par un pointeur de base.

Les fonctions virtual sont liées au code associé à la classe de l'objet, plutôt que à la classe du pointeur ou de la référence.Lorsque vous écrivez delete basePtr, et que la classe de base à un destructeur virtual, le destructeur appelée est celui associé à au type de l'objet *basePtr, plutôt que celui associé au type du pointeur. Il s'agit généralement d'une bonne chose.

ATTENTION; POUR PERSONNES AVERTIES.
Techniquement parlant, vous avez besoin de déclarer le destructeur de la classe de base virtuel si et seulement si vous pensez permettez à quelqu'un d'appeler le destructeur de l'objet par un pointeur de la classe de base (cela est normalement réalisé implicitement par un delete), et si l'objet à détruire et d'une classe dérivée qui a un destructeur non trivial. Une classe a un destructeur non trivial si elle a ou un destructeur explicitement défini, ou si elle a un obket member ou une classe de base qui a un destructeur non trivial (notez bien qu'il s'agit d'une définition récursive (e.g. une classe a un destructeur non trivial si elle a un objet membre(qui a une classe de base (qui a un objet membre(qui a une classe de base (qui a un destructeur explicitement défini)))))
FIN ATTENTION; POUR PERSONNES AVERTIES

Si vous avez du mal a comprendre la règle précédente, essayez celle ci (ultra)simplifiée en longueur. Une classe doit avoir un destructeur virtuel a moins que cette classe n'ait pas de fonctions virtuelles. Donc: si vous avez des fonctions virtuelles, vous allez certainement effectuer des opérations sur les objets dérivés via le pointeur de base, et certaines de ces opérations peuvent appeler le destructeur (normalement appelé implicitement par delete). De plus, une fois que vous avez mis votre première fonction virtuelle au sein de la classe, vous avez déjà réservé l'espace de stockage supplémentaire par objet (un pointeur par objet, notez que cela est théoriquement spécifique au compilteur; en pratique, tout le monde fait plus ou moins la même chose), donc mettre le destructeur virtuel ne vous coûtera généralement rien de plus.

[ Haut | Bas | Rechercher ]


[20.6] Qu'est ce qu'un "constructeur virtuel" ?
Un idiome qui vous permet de faire ce que le C++ ne supporte pas directement.

Vous pouvez obtenir l'effet d'un constructeur virtuel par l'utilisation d'une fonction membre virtual clone() (construction par copie), ou une fonction membre virtual create() (pour le constructeur par défaut).

class Shape {
public:
virtual ~Shape() { } // Un destructeur virtual
virtual void draw() = 0; // Une fonction virtuelle pure
virtual void move() = 0;
// ...
virtual Shape* clone() const = 0; // Utilises la construction par copie
virtual Shape* create() const = 0; // Utilises le constructeur par défaut
};

class Circle : public Shape {
public:
Circle* clone() const { return new Circle(*this); }
Circle* create() const { return new Circle(); }
// ...
};

Dans la fonction membre clone(), le code new Circle(*this) appelle le constructeur de copie de Circle pour copier l'état de this dans le nouvel objet Circle. (C'est un clone). Dans la fonction membre create(), le code new Circle() appelle le constructeur par défaut de Circle..

Les clients les utilisent comme s'il s'agissait de "constructeurs virtual":

void userCode(Shape& s)
{
Shape* s2 = s.clone();
Shape* s3 = s.create();
// ...
delete s2; // Vous avez probablement besoin d'un destructeur virtuel ici.
delete s3;
}

Cette méthode fonctionnera correctement que Shape soit un Circle, Square, ou une autre généralisation (kind-of) de Shape qui n'existe pas encore. Il permet donc la création d'un objet du même type que celui que vous traitez sans que vous en ayez connaissance puisque vous travaillez avec le type de base.

Note: Le type de retour de la fonction membre clone() de Circle est intentionnellement différente du type de retour de la fonction membre clone()de Shape. Cela s'appelle un type de retour covariant (Covariant Return Types), une fonctionnalité qui n'était originellement pas partie intégrante du langage. Si votre compilateur se plaint de la déclaration Circle* clone() const dans la classe Circle (e.g., avec un message du genre "The return type is different" ou "The member function's type differs from the base class virtual function by return type alone"), vous avez un vieux compilateur et vous avez besoin de changer le type de retour en Shape*.

[ Haut | Bas | Rechercher ]


E-mail Marshall Cline Ecrire à l'auteur, au traducteur, ou en savoir plus sur la traduction.
C++ FAQ Lite fr | Table des matières | Index | A propos de l'auteur | © | Téléchargez votre propre copie ]
Dernière révision Sun Apr 13 23:54:40 PDT 2003