[21] Héritage - l'héritage approprié et la substituabilité

(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 [21]


[21.1] Dois-je cacher les fonctions membres qui étaient publiques dans ma classe de base ?
Ne jamais, ô grand jamais faire ceci. Jamais. Jamais!

L'intention de cacher (d'éliminer, de révoquer, ou de privatiser) des fonctions membres publiques héritées est une erreur de conception bien trop fréquente. Cela provient généralement d'une pensée un peu trop confuse.

(Note: cette FAQ est relative à l'héritage public; l'héritage privé et l'héritage protégé sont différents.)

[ Haut | Bas | Rechercher ]


[21.2] Derived* -> Base* fonctionne bien; pourquoi Derived** -> Base** ne fonctionne t'il pas?

C++ permet à un Derived* d'être converti en un Base*, car un objet Derived est une sorte d'objet Base. Cependant essayer de convertir un Derived** en un Base** est considéré comme une erreur. Bien que cette erreur ne sois pas flagrante, c'est loin d'être une bonne chose. Par exemple, on pourrait convertir un Car** en un Vehicle**, et similairement on pourrait convertir un NuclearSubmarine** en un Vehicle**, on pourrait affecter ces deux pointeurs et finir avec un Car* qui pointe sur un NuclearSubmarine:

class Vehicle {
public:
virtual ~Vehicle() { }
virtual void startEngine() = 0;
};

class Car : public Vehicle {
public:
virtual void startEngine();
virtual void openGasCap();
};

class NuclearSubmarine : public Vehicle {
public:
virtual void startEngine();
virtual void fireNuclearMissle();
};

int main()
{
Car car;
Car* carPtr = &car;
Car** carPtrPtr = &carPtr;
Vehicle** vehiclePtrPtr = carPtrPtr; // Ceci est une erreur en C++
NuclearSubmarine sub;
NuclearSubmarine* subPtr = ⊂
*vehiclePtrPtr = subPtr; // Cette ligne aurait permis à carPtr de pointer sur sub !
carPtr->openGasCap(); // Cela aurait pu déclencher le tir d'un missile nucléaire via fireNuclearMissle()!
}

En d'autres termes, si il était légal de convertir un Derived** en un Base**, le Base** aurait pu être déréférencé et le Base* aurait pu être pointé sur un objet d'une classe dérivée différente qui aurait pu causer de sérieux problèmes à la sécurité nationale (qui sait ce qu'il serait advenu si vous aviez voulu ouvrir le bouchon de réservoir via la fonction membre openGasCap() sur ce que vous pensiez être une voiture alors qu'il s'agissait en réalité d'un sous-marin nucléaire!! Essayez le code ci dessus -- sur la plupart des compilateurs cela appellera NuclearSubmarine::fireNuclearMissile()!

(Note: cette FAQ est relative à l'héritage public; l'héritage privé et l'héritage protégé sont différents.)

[ Haut | Bas | Rechercher ]


[21.3] un parking-de-voiture est-il une sorte-de parking-de-véhicule?
Non.

Je sais que cela peut paraître étrange, mais c'est la réalité. Vous pouvez voir cela comme la conséquence directe de la question précédente ou vous pouvez le voir de la façon suivante: Si la relation sorte-de était valide, alors cela signifierait que l'on aurait pu faire pointer le pointeur de parking-lot-of-Vehicle sur un parking-lot-of-Car. Mais parking-lot-of-Vehicle défini la fonction membre addNewVehicleToParkingLot(Vehicle&) qui permet d'ajouter n'importe quel objet véhicule au parking. Cela permet donc de parquer un NuclearSubmarine dans un parking-lot-of-Car. Il peut être surprenant de sortir du parking ce que l'on pensait être une voiture alors qu'il s'agit en réalité d'un sous-marin nucléaire.

Encore une autre manière d'énoncer cette vérité vraie: un contenant de truc n'est pas une sorte de contenant de n'importe quoi, même si un truc est une sorte de n'importe quoi. Vous pouvez déglutir de nouveau; c'est la vérité.

On ne vous demande pas d'aimer, mais vous devez l'accepter.

Un dernier exemple utilisé dans nos formations OO/C++: "un sac de pommes n'est pas un sac de fruits" Si un sac de pommes peut être assimilé à un sac de fruits alors quelqu'un aurait trés bien pu glisser une banane dans ce sac alors qu'il n'était censé contenir que des pommes!

(Note: cette FAQ est relative à l'héritage public; l'héritage privé et l'héritage protégé sont différents.)

[ Haut | Bas | Rechercher ]


[21.4] Est ce qu'un tableau de Derived est une-sorte-de (kind-of) tableau de Base?
[Recently added return type to main() (on 10/99).]

Non.

C'est le corollaire de la FAQ précédente. Malheureusement, celui là peut vous amener pas mal d'ennui. Considérez ceci:

class Base {
public:
virtual void f(); // 1
};

class Derived : public Base {
public:
// ...
private:
int i_; // 2
};

void userCode(Base* arrayOfBase)
{
arrayOfBase[1].f(); // 3
}

int main()
{
Derived arrayOfDerived[10]; // 4
userCode(arrayOfDerived); // 5
}

Le compilateur pense que le typage est parfaitement correct. La ligne 5 convertit un Derived* en un Base*. En réalité cela est diaboliquement insidieux: sachant que Derived a une taille supérieure à Base, l'arithmétique du pointeur effectué ligne 3 est incorrect: le compilateur utilise (sizeof(Base)) quand il calcule l'adresse de arrayOfBase[1], alors qu'il s'agit d'un Derived, ce qui signifie que l'adresse calculée (et par la meme l'appel de la fonction f() ) n'est meme pas sur le debut d'un objet ! Elle est en plein milieu de l'objet Derived. En supposant que votre compilateur utilise l'approche classique des fonctions virtuelles , cela va réinterpréter le i_ du premier Derived comme si il pointait sur la table virtuelle, va suivre l'adresse désigné par le pointeur (ce qui signifie que l'on va aller chercher une valeur dans un emplacement mémoire de manière aléatoire) et récupérer les premiers mots de cet emplacement mémoire et l'interpreter comme si il s'agissait de l'adresse d'une fonction membre C++, la chargera en mémoire (emplacement mémoire aléatoire) et commencera à interpréter les instructions machines de cet emplacement. La probabilité de plantage est plus qu'élevée.

La racine du problème est que le C++ ne peut pas distinguer un pointeur-sur-truc d'un pointeur-sur-tableau-de-trucs. Naturellement le C++ hérite cette fonctionnalité du C.

NOTE: Si nous avions utilisé une classe array-like (e.g, vector<Derived> de la STL ) plutôt qu'un tableau brut, le problème aurait été dénoncé lors de la compilation plutôt que lors d'un résultat d'une exécution désastreuse.

(Note: cette FAQ est relative à l'héritage public; l'héritage privé et l'héritage protégé sont différents.)

[ Haut | Bas | Rechercher ]


[21.5] Est-ce mauvais qu'un tableau de Derived ne soit pas une-sorte-de tableau de Base ?
Oui, les tableaux sont un fléau. (je plaisante à moitié).

Sérieusement, les tableaux sont trés proches des pointeurs et les pointeurs sont notoirement difficiles à gérer. Mais si vous avez parfaitement saisi en quoi les FAQs ci-avant sont un problème d'un point de vue conceptuel (e.g. si vous savez pourquoi un container de Truc n'est pas une sorte-de container de n'importe quoi), et que vous pensez que ceux qui vont maintenir votre code ont également parfaitement saisi ces concepts OO, alors vous pouvez utiliser les tableaux. Mais si vous êtes comme la majorité des gens, vous devriez utiliser un container sous forme de template tel que vector<T> de la STL plutôt que des tableaux.

(Note: cette FAQ est relative à l'héritage public; l'héritage privé et l'héritage protégé sont différents.)

[ Haut | Bas | Rechercher ]


[21.6] Un cercle est t'il une sorte-d'ellipse?
Non, même si une Ellipseest susceptible de changer sa taille asymétriquement.

Par exemple, supposons que Ellipse a une méthode membre setSize(x,y), et supposons que cette fonction membre implique que width() de Ellipse sera x, et que height() sera y. Alors dans ce cas, Circle ne peut être une sorte-de Ellipse. Simplement, si Ellipse peut faire quelque chose que Circle ne peut pas, alors Circle n'est pas une sorte-de Ellipse.

Cela laisse deux relations potentielles (valides) entre Circle et Ellipse:

Dans le premier cas Ellipse peut être dérivé de AsymmetricShape, et setSize(x,y)peut être introduit dans AsymmetricShape. Cependant Circlepeut être dérivé de SymmetricShapequi a une fonction membre setSize(size).

Dans le second cas, la classe Oval peut avoir uniquement setSize(size) qui défini à la fois width() et height(). Ellipse et Circle peuvent tout les deux hérités de Oval. Ellipse -mais pas Circle- peut définir l'opération setSize(x,y) (mais attention aux méthodes cachées si la même fonction membre nommée setSize() est utilisée pour les deux opérations).

(Note: cette FAQ est relative à l'héritage public; l'héritage privé et l'héritage protégé sont différents.)

(Note: setSize(x,y) n'est pas sacrée. Suivant votre but, il peut être correct de ne pas permettre aux utilisateurs de changer les dimensions de Ellipse, dans ce cas il peut etre conceptuellement correct de ne pas avoir une méthode setSize(x,y) dans Ellipse. Cependant cette série de FAQs discute de la technique à adopter quand vous souhaitez créer une classe dérivée d'une classe pré-existante qui contient une méthode "inacceptable". Bien sûr, la situation idéale est de découvrir ce problème quand la classe de base n'existe pas encore. Mais la vie n'est pas toujours parfaite...)

[ Haut | Bas | Rechercher ]


[21.7] Y-a-t'il d'autres options concernant le dilemne "un cercle est/n'est pas une sorte-d'ellipse" ?
Si vous souhaitez que toutes les Ellipses puissent être dimensionnées asymétriquement et que vous souhaitez que Circlene puisse pas être dimensionné asymétriquement, clairement, vous devez choisir l'un des deux. Donc vous avez soit à éliminer Ellipse::setSize(x,y), soit à éliminer la relation d'héritage entre Circle et Ellipse, où vous devez admettre que vos cercles Circlene sont pas nécessairement circulaires.

Voici les deux plus grands pièges dans lequels les programmeurs C++/OO tombent régulièrement. Ils essayent d'utiliser de bidouiller du code pour réparer une conception hasardeuse (ils redéfinissent Circle::setSize(x,y)qui lance une exception, appellent abort(), choisissent la moyenne des deux paramètres ou en font une fonction vide). Malheureusement, chacune de ces bidouilles surprendra l'utilisateur car il s'attendra à trouver width() == x and height() == y. La seule chose que vous ne devez pas faire est de surprendre vos utilisateurs.

Si il est important pour vous de garder la relation d'héritage: "Circle est une sorte-de Ellipse", vous pouvez affaiblir la promesse faite par setSize(x,y) de Ellipse. E.g., vous pouvez changer cette promesse en "Cette fonction membre peut mettre width() à x et/ou peut mettre height() à y, ou peut ne rien faire". Malheureusement cela dilue fortement la promesse initiale car l'utilisateur ne peut pas se baser sur un comportement significatif. La hiérarchie entière commence donc à devenir un peu chancelante. (Il est difficile de convaincre quelqu'un d'utiliser un objet si vous avez à hausser vos épaules quand on vous demande ce qu'il fait réellement)

(Note: cette FAQ est relative à l'héritage public; l'héritage privé et l'héritage protégé sont différents.)

(Note: setSize(x,y) n'est pas sacrée. Suivant votre but, il peut être correct de ne pas permettre aux utilisateurs de changer les dimensions de Ellipse, dans ce cas il peut etre conceptuellement correct de ne pas avoir une méthode setSize(x,y) dans Ellipse. Cependant cette série de FAQs discute de la technique à adopter quand vous souhaitez créer une classe dérivée d'une classe pré-existante qui contient une méthode "inacceptable". Bien sûr, la situation idéale est de découvrir ce problème quand la classe de base n'existe pas encore. Mais la vie n'est pas toujours parfaite...)

[ Haut | Bas | Rechercher ]


[21.8] Mais, j'ai une thèse en Mathématique, et je suis sûr qu'un cercle est une sorte d'ellipse! Cela signifie t'il que Marshall Cline (N.D.T.: l'auteur de cette FAQ) est idiot? Ou que le C++ est idiot? Ou que l'OO est idiot?
En fait, cela ne signifie rien de cela. La triste réalité, c'est que cela signifie que votre intuition est mauvaise.

Ecoutez, j'ai reçu et répondu à des douzaines de courrier électronique passionnés par ce sujet. Je l'ai enseigné des centaines de fois à des milliers de professionnels du logiciel un peu partout. Je sais que cela est contre votre intuition. Mais vous pouvez me croire, votre intuition est mauvaise.

Le réel problème, c'est que votre notion intuitive de "sorte-de" ne correspond pas à la notion OO d'héritage(techniquement appelé le sous-typage). L'essentiel, c'est que les objets de la classe dérivée doivent être substituable aux objets de la classe de base. Dans le cas cercle/ellipse, la fonction membre setSize(x,y) viole cette substituabilité.

Vous avez trois possibilités: [1] vous supprimez la fonction membre setSize(x,y) de Ellipse (et donc vous cassez le code existant qui appelle la fonction membre setSize(x,y)), [2] vous permettez à Circle d'avoir une hauteur et une largeur différente (hum..un cercle asymétrique...), ou [3] vous abandonnez la relation d'héritage. Désolé, mais il n'y a simplement pas d'autres choix. Notez que certaines personnes mentionnent également la possibilité de dériver Circle et Ellipse d'une classe de base commune, mais il s'agit juste d'une variante de l'option [3].

Une autre façon de dire cela, c'est que vous avez, soit à rendre votre classe de base fonctionnellement moins riche (cela revient à modifier Ellipse de manière à ce que l'on ne puisse pas mettre sa largeur différente de sa hauteur), ou rendre la classe dérivée fonctionnellement plus riche (dans ce cas rendre possible le dimensionnement de Circle de manière symétrique et, hum..asymétrique). Quand aucun de ces deux possibilité n'est satisfaisante ( comme dans le cas cercle/ellipse), générallement on supprime la relation d'héritage. Si la relation d'héritage doit exister, alors vous aurez certainement à supprimer les fonctions membres mutantes (setHeight(y), setWidth(x), and setSize(x,y)) de la classe de base.

(Note: cette FAQ est relative à l'héritage public; l'héritage privé et l'héritage protégé sont différents.)

(Note: setSize(x,y) n'est pas sacrée. Suivant votre but, il peut être correct de ne pas permettre aux utilisateurs de changer les dimensions de Ellipse, dans ce cas il peut etre conceptuellement correct de ne pas avoir une méthode setSize(x,y) dans Ellipse. Cependant cette série de FAQs discute de la technique à adopter quand vous souhaitez créer une classe dérivée d'une classe pré-existante qui contient une méthode "inacceptable". Bien sûr, la situation idéale est de découvrir ce problème quand la classe de base n'existe pas encore. Mais la vie n'est pas toujours parfaite...)

[ Haut | Bas | Rechercher ]


[21.9] Perhaps Ellipse should inherit from Circle then?

If Circle is the base class and Ellipse is the derived class, then you run into a whole new set of problems. For example, suppose Circle has a radius() method. Then Ellipse will also need to have a radius() method, but that doesn't make much sense: what does it even mean for a possibly assymetric ellipse to have a radius?

If you get over that hurdle, such as by having Ellipse::radius() return the average of the major and minor axes or whatever, then there is a problem with the relationship between radius() and area(). Suppose Circle has an area() method that promises to return 3.14159[etc] times the square whatever radius() returns. Then either Ellipse::area() will not return the true area of the ellipse, or you'll have to stand on your head to get radius() to return something that matches the above formula.

Even if you get past that one, such as by having Ellipse::radius() return the square root of the ellipse's area divided by pi, you'll get stuck by the circumference() method. Suppose Circle has a circumference() method that promises to return two times pi times whatever is returned by radius(). Now you're stuck: there's no way to make all those constraints work out for Ellipse: the Ellipse class will have to lie about its area, its circumference, or both.

Bottom line: you can make anything inherit from anything provided the methods in the derived class abide by the promises made in the base class. But you ought not to use inheritance just because you feel like it, or just because you want to get code reuse. You should use inheritance (a) only if the derived class's methods can abide by all the promises made in the base class, and (b) only if you don't think you'll confuse your users, and (c) only if there's something to be gained by using the inheritance — some real, measurable improvement in time, money or risk.

[ Haut | Bas | Rechercher ]


[21.10] Mais mon problème n'a rien à voir avec les cercles et les ellipses, alors qu'est ce que ce stupide exemple m'apporte ?
Ahhh, voilà le hic!. Vous pensez que l'exemple cercle/ellipse est juste un stupide exemple. Mais en réalité, votreproblème est un isomorphisme de cet exemple.

Je me moque de savoir votre problème d'héritage, mais tout(oui tout) les mauvais héritages se réduisent à l'exemple du cercle-qui-n'est-pas-une-sorte-d'ellipse.

Voici pourquoi: Les mauvais héritages ont tout le temps une classe de base avec une action bien spécifique(la plupart du temps une fonction membre ou deux, parfois une promesse effectuée par la combinaison de fonctions membres) qu'une classe dérivée ne peut satisfaire. Vous avez soit à alléger votre classe de base d'un point de vue fonctionnel, augmenter les fonctionnalités de votre classe dérivée ou éliminer la relation d'héritage. J'ai vu de nombreux exemple de propositions d'héritages, et croyez moi, ils se réduisent tous à l'exemple cercle/ellipse.

Donc, si vous comprenez vraiment le problème cercle/ellipse, vous serez capable de reconnaître des problèmes d'héritages un peu partout. Si vous ne comprenez pas ce qu'il se trame sous le problème cercle/ellipse, il y a de fortes chances que vous allez faire de trés graves et de très coûteuses erreurs.

Malheureusement, c'est la vérité.

(Note: cette FAQ est relative à l'héritage public; l'héritage privé et l'héritage protégé sont différents.)

[ Haut | Bas | Rechercher ]


[21.11] How could "it depend"??!? Aren't terms like "Circle" and "Ellipse" defined mathematically?

It's irrelevant that those terms are defined mathematically. That irrelevance is why "it depends."

The first step in any rational discussion is to define terms. In this case, the first step is to define the terms Circle and Ellipse. Believe it or not, most heated disagreements over whether class Circle should/shouldn't inherit from class Ellipse are caused by incompatible definitions of those terms.

The key insight is to forget mathematics and "the real world," and instead accept as final the only definitions that are relevant for answering the question: the classes themselves. Take Ellipse. You created a class with that name, so the one and only final arbiter of what you meant by that term is your class. People who try to mix "the real world" into the discussion get hopelessly confused, and often get into heated (and, sadly, meaningless) arguments.

Since so many people just don't get it, here's an example. Suppose your program says class Foo : public Bar { ... }. This defines what you mean by the term Foo: the one, final, unambiguous, precise definition of Foo is given by unioning the public parts of Foo with the public parts of its base class, Bar. Now suppose you decide to rename Bar to Ellipse and Foo to Circle. This means that you (yes you; not "mathematics"; not "history"; not "precedence" nor Euclid nor Euler nor any other famous mathematician; little old you) have defined the meaning of the term Circle within your program. If you defined it in a way that didn't correspond to people's intuitive notion of circles, then you probably should have chosen a better label for your class, but nonetheless your definition is the one, final, unambiguous, precise definition of the term Circle in your program. If somebody else outside your program defines the same term differently, that other definition is irrelevant to questions about your program, even if the "somebody else" is Euclid. Within your program, you define the terms, and the term Circle is defined by your class named Circle.

Simply put, when we are asking questions about words defined in your program, we must use your definitions of those terms, not Euclid's. That is why the ultimate answer to the question is "it depends." It depends because the answer to whether the thing your program calls Circle is properly substitutable for the thing your program calls Ellipse depends on exactly how your program defines those terms. It's ridiculous and misleading to use Euclid's definition when trying to answer questions about your classes in your program; we must use your definitions.

When someone gets heated about this, I always suggest changing the labels to terms that have no predetermined connotations, such as Foo and Bar. Since those terms do not evoke any mathematical relationships, people naturally go to the class definition to find out exactly what the programmer had in mind. But as soon as we rename the class from Foo to Circle, some people suddenly think they can control the meaning of the term; they're wrong and silly. The definition of the term is still spelled out exclusively by the class itself, not by any outside entity.

Next insight: inheritance means "is substitutable for." It does not mean "is a" (since that is ill defined) and it does not mean "is a kind of" (also ill defined). Substitutability is well defined: to be substitutable, the derived class is allowed (not required) to add (not remove) public methods, and for each public method inherited from the base class, the derived class is allowed (not required) to weaken preconditions and/or strengthen postconditions (not the other way around). Further the derived class is allowed to have completely different constructors, static methods, and non-public methods.

Back to Ellipse and Circle: if you define the term Ellipse to mean something that can be resized asymmetrically (e.g., its methods let you change the width and height independently and guarantee that the width and height will actually change to the specified values), then that is the final, precise definition of the term Ellipse. If you define the thing called Circle as something that cannot be resized asymmetrically, then that is also your prerogative, and it is the final, precise definition of the term Circle. If you defined those terms in that way, then obviously the thing you called Circle is not substitutable for the thing you called Ellipse, therefore the inheritance would be improper. QED.

So the answer is always "it depends." In particular, it depends on the behaviors of the base and derived classes. It does not depend on the name of the base and derived classes, since those are arbitrary labels. (I'm not advocating sloppy names; I am, however, saying that you must not use your intuitive connotation of a name to assume you know what a class does. A class does what it does, not what you think it ought to do based on its name.)

It bothers (some) people that the thing you called Circle might not be substitutable for the thing you called Ellipse, and to those people I have only two things to say: (a) get over it, and (b) change the labels of the classes if that makes you feel more comfortable. For example, rename Ellipse to ThingThatCanBeResizedAssymetrically and Circle to ThingThatCannotBeResizedAssymetrically.

Unfortunately I honestly believe that people who feel better after renaming the things are missing the point. The point is this: in OO, a thing is defined by how it behaves, not by the label used to name it. Obviously it's important to choose good names, but even so, the name chosen does not define the thing. The definition of the thing is specified by the public methods, including the contracts (preconditions and postconditions) of those methods. Inheritance is proper or improper based on the classes' behaviors, not their names.

[ Haut | Bas | Rechercher ]


[21.12] If SortedList has exactly the same public interface as List, is SortedList a kind-of List?

Probably not.

The most important insight is that the answer depends on the details of the base class's contract. It is not enough to know that the public interfaces / method signatures are compatible; one also needs to know if the contracts / behaviors are compatible.

The important part of the previous sentence are the words "contracts / behaviors." That phrase goes well beyond the public interface = method signatures = method names and parameter types and constness. A method's contract means its advertised behavior = advertised requirements and promises = advertised preconditions and postconditions. So if the base class has a method void insert(const Foo& x), the contract of that method includes the signature (meaning the name insert and the parameter const Foo&), but goes well beyond that to include the method's advertised preconditions and postconditions.

The other important word is advertised. The intention here is to differentiate between the code inside the method (assuming the base class's method has code; i.e., assuming it's not an unimplemented pure virtual function) and the promises made outside the method. This is where things get tricky. Suppose List::insert(const Foo& x) inserts a copy of x at the end of this List, and the override of that method in SortedList inserts x in the proper sort-order. Even though the override behaves in a way that is incompatible with the base class's code, the inheritance might still be proper if the base class makes a "weak" or "adaptable" promise. For example, if the advertised promise of List::insert(const Foo& x) is something vague like, "Promises a copy of x will be inserted somewhere within this List," then the inheritance is probably okay since the override abides by the advertised behavior even though it is incompatible with the implemented behavior.

The derived class must do what the base class promises, not what it actually does.

The key is that we've separated the advertised behavior ("specification") from implemented behavior ("implementation"), and we rely on the specification rather than the implementation. This is very important because in a large percentage of the cases the base class's method is an unimplemented pure virtual — the only thing that can be relied on is the specification -- there simply is no implementation on which to rely.

Back to SortedList and List: it seems likely that List has one or more methods that have contracts which guarantee order, and therefore SortedList is probably not a kind-of List. For example, if List has a method that lets you reorder things, prepend things, append things, or change the ith element, and if those methods make the typical advertised promise, then SortedList would need to violate that advertised behavior and the inheritance would be improper. But it all depends on what the base class advertises — on the base class's contract.

[ 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:33 PDT 2003