 |
3. HERITAGE |
L'héritage est certainement l'un des concepts les plus novateurs de la programmation
orientée objet. Dans ce chapitre, nous rappelons le concept de polymorphisme dynamique
lié à la virtualité et à la redéfinition de méthodes. En outre, le
C++ est un langage qui autorise l'héritage multiple, il s'agit d'un concept très
souvent critiqué, puisqu'il soulève quelques problèmes
d'implémentation. Nous le présentons tout de même, car il peut s'avérer
utile dans certaines situations. Nous expliquons alors une alternative à l'héritage
multiple, à travers la notion d'interface proposée notamment dans le langage Java.
L'héritage permet de voir un objet comme appartenant à une catégorie plus
ou moins précise. Il est donc fondamental de disposer d'un mécanisme, nommé
RTTI (Run-Time Type Information) pour le langage C++, qui permet de déterminer la
nature exacte d'un objet, notamment au moment de l'exécution du programme. Nous ne
l'abordons que très brièvement dans ce chapitre. Enfin, bien que le sujet semble
quelque peu éloigné de l'héritage, nous avons choisi de présenter ici
les exceptions, un mécanisme qui offre une manière plus élégante de
gérer les erreurs dans un programme.
Le terme polymorphisme est employé pour indiquer qu'un élément peut
prendre plusieurs formes. Il existe deux types de polymorphisme: le polymorphisme statique et le
polymorphisme dynamique. Celui qui nous intéresse ici est le dynamique et concerne plus
particulièrement les méthodes. Il signifie que le contenu d'une méthode n'est
définitivement établi qu'au moment de son appel. La combinaison de l'héritage,
de la virtualité et la possibilité de redéfinir des méthodes permettent ce
polymorphisme dynamique.
Avant de s'attaquer au polymorphisme, intéressons-nous simplement à
l'héritage. Une classe permet d'identifier une catégorie de variables dans un
programme, en l'occurrence des objets. L'héritage est un moyen d'organiser ces
catégories. Il permet de définir des catégories générales dans
lesquelles seront réunies des catégories plus précises. Par exemple, la
catégorie (ou classe) Animal peut être précisée (on dit
plutôt spécialisée) en sous-catégories comme
Mammifere, Poisson... En programmation, cette phase de
spécialisation a un intérêt particulier: elle permet de factoriser le code, les
parties communes aux classes Mammifere et Poisson peuvent
être placées dans la classe Animal. Prenons l'exemple suivant où
l'on souhaite positionner sur une carte des animaux identifiés par un nom (e.g. on
établi les bases d'une simulation).
class Animal {
protected:
Chaine _nom; // Nom de l'animal.
int _x; // Abscisse de
sa position.
int _y; //
Ordonnée de sa position.
public:
Animal(char * n,int x,int y) : _nom(n), _x(x), _y(y) {}
const Chaine & getNom(void) const { return _nom; }
int getX(void)
const { return _x; }
int getY(void)
const { return _y; }
bool estFemelle(void)
const { ... }
};
| |
| Méthode virtuelle, redéfinition et polymorphisme |
| |
La définition précédente de la classe Animal est bien entendue
incomplète. Supposons par exemple que Animal possède les méthodes
deplacer et engendrer, la première déplace l'animal et la
seconde, applicable seulement à une femelle, consiste à mettre bas et donc engendrer
un nouvel Animal. Les poissons et les mammifères ne se déplacent et ne
mettent pas bas de la même manière. Les méthodes sont donc
déclarées dans la classe Animal, puisqu'elles sont communes aux deux
sous-classes. En revanche, le contenu même des méthodes doit être fourni par
chaque sous-classe. Voici les prototypes rajoutés à la classe Animal.
virtual void deplacer(void);
virtual Animal * engendrer(void);
Notez que les deux méthodes sont virtuelles. Cela signifie qu'elles peuvent avoir un
corps mais que les sous-classes sont autorisées à le remplacer (cette action
étant appelée redéfinition). Voici la définition des classes
Poisson et Mammifere.
class Poisson : public Animal {
protected:
int _profondeur; // Profondeur où vit ce genre de poisson.
public:
void deplacer(void) { ... }
Animal * engendrer(void) { if (estFemelle()) { ... } }
};
class Mammifere : public Animal {
protected:
int _vitesse; // Vitesse de déplacement.
public:
void deplacer(void) { ... }
Animal * engendrer(void) { if (estFemelle()) { ... } }
};
Les méthodes redéfinies dans Poisson et Mammifere sont
bien sûr différentes. Notez qu'une fois qu'une méthode est
déclarée virtuelle dans une classe, elle est automatiquement virtuelle dans les
sous-classes, même si le mot-clé virtual n'est pas précisé. Supposons maintenant un
tableau d'animaux, auxquels on applique la méthode deplacer.
int i = 0;
Animal t[] = { Poisson("Maurice",10,20,3),
Mammifere("Rantanplan",5,9,17)
};
while (i<n) {
t[i].deplacer();
++i;
}
Si vous vous attendiez à ce que Maurice se déplace comme un poisson et Rantanplan
comme un chien, et bien c'est perdu ! En effet, quand le poisson est placé dans la
première case du tableau, il est converti en un animal (par le constructeur de copie de
cette classe). De la même manière, le mammifère redevient un simple animal dans
le tableau. En résumé, c'est la méthode deplacer de
Animal qui est appelée dans les deux cas. Voici la solution qui fait que chaque
animal se déplace de la bonne manière. Ce qu'on appelle polymorphisme
dynamique, c'est le fait que, au moment de la compilation, on ne sait pas quelle méthode
deplacer va véritablement être appelée, c'est seulement au moment
de l'exécution que tout se décidera.
int i = 0;
Animal * t[] = { new Poisson("Maurice",10,20,3),
new
Mammifere("Rantanplan",5,9,17) };
while (i<n) {
t[i]->deplacer();
++i;
}
Il faut savoir que la virtualité d'une méthode l'empêche d'être
déclarée inline, tout simplement parce que le mécanisme d'appel
est différent d'une méthode simple et qu'il ne peut pas être
évité (en effet, c'est à l'exécution qu'on décide
réellement du contenu de la méthode appelée, il est donc impossible à
la compilation de remplacer directement cet appel par un contenu). Et avec les
références, qu'est-ce que ça donne ?
int i = 0;
Animal & t[] = { Poisson("Maurice",10,20,3),
Mammifere("Rantanplan",5,9,17)
};
while (i<n) {
t[i].deplacer();
++i;
}
Encore perdu ! On ne peut pas faire un tableau de références. Mais les
références permettent le polymorphisme dynamique. Prenons l'exemple suivant.
void deplacer(Animal & a) {
std::cout << "Je deplace " << a.getNom() << std::endl;
a.deplacer();
}
...
int i = 0;
Animal * t[] = { new Poisson("Maurice",10,20,3),
new
Mammifere("Rantanplan",5,9,17) };
while (i<n) {
deplacer(*(t[i]));
++i;
}
Dans la fonction deplacer, l'argument reçu est une référence
sur un Animal. Il n'y a donc pas de copie, l'animal est alors soit Maurice, soit
Rantanplan. La nature précise de l'animal sera déterminée seulement à
l'exécution, il y a donc bien polymorphisme dynamique.
Lorsqu'on déclare une méthode virtuelle dans une classe, il faut
impérativement que le destructeur soit virtuel. La raison se trouve tout simplement au
niveau de la destruction de l'objet. Prenons l'exemple suivant.
Animal * a = new Poisson("Maurice",10,20,3);
...
delete a;
Si le destructeur n'est pas virtuel, alors seul le destructeur de Animal est
appelé. En revanche, s'il est virtuel, le destructeur de Poisson est
également appelé. Mais le mécanisme est à l'image de la construction,
c'est-à-dire que les destructeurs des super-classes sont appelés dans l'ordre inverse
de la construction. Ainsi, pour notre exemple, le destructeur de Poisson est
appelé avant celui de Animal.
De la même manière qu'il existe des pointeurs de fonction en C, il est possible de
définir des pointeurs de méthode en C++. Supposons par exemple qu'on souhaite
écrire une fonction qui parcourt tous les animaux d'un tableau et appelle une méthode
donnée sur chacun des objets.
void parcourir(Animal * t[],int n,void (Animal::*m)(void)) {
int i = 0;
while (i<n) (t[i++]->*m)();
}
L'argument m représente un pointeur de méthode, la seule
différence avec un pointeur de fonction est que le nom de la classe apparaît au moment
de la déclaration du type. Sinon, le format général d'un pointeur de
méthode est très proche de celui d'un pointeur de fonction.
type_retour
(nom_classe::*nom_variable)(type_argument_1,type_argument_2...)
Voici maintenant comment utiliser la fonction parcourir, l'exemple appelle la
méthode deplacer de tous les animaux contenus dans le tableau.
Poisson maurice("Maurice",10,20,3);
Mammifere rantanplan("Rantanplan",5,9,17);
Animal * t[] = { &maurice, &rantanplan };
parcourir(t,2,&Animal::deplacer);
Est-ce qu'on aurait pas oublié Flipper dans l'histoire ? Oui, je sais, c'est un
mammifère (en particulier il met bas de la même manière), mais avouez
qu'il se déplace plutôt comme un poisson. Donc je propose qu'on déclare une
classe Dauphin qui hérite à la fois de Poisson et de
Mammifere. Attention, notez bien que tous les langages orientés objet
n'autorisent pas cette manipulation. Voici la déclaration de la classe.
class Dauphin : public Poisson, public Mammifere {
...
public:
void deplacer(void) { Poisson::deplacer(); }
Animal * engendrer(void) { return Mammifere::engendrer(); }
};
Cette déclaration soulève tout de même quelques problèmes. Tout
d'abord, le dauphin hérite de la méthode deplacer à la fois de
Poisson et de Mammifere. Il faut donc choisir laquelle sera effectivement
appelée quand on voudra déplacer un dauphin. La solution est dans le corps de la
méthode deplacer: celle-ci est redéfinie, et à l'aide de
l'opérateur ::, on appelle la méthode deplacer de la
super-classe qu'on souhaite (ici c'est Poisson).
Le second problème se situe dans la construction même des instances. Revenons aux
classes Poisson et Mammifere, et penchons-nous sur leur constructeur.
Poisson::Poisson(char * n,int x,int y,int p)
: Animal(n,x,y), _profondeur(p) {}
Mammifere::Mammifere(char * n,int x,int y,int v)
: Animal(n,x,y), _vitesse(v) {}
Nous avons vu aux chapitres précédents qu'avant l'appel au constructeur d'une
classe, les attributs étaient construits. En fait, dans le cas d'un héritage, avant
même la construction des attributs, il y a la construction de la partie de l'objet issue des
super-classes. Ainsi, pour les classes Poisson et Mammifere, il faut
construire la partie Animal avant la partie propre à chaque sous-classe. Dans
le cas maintenant de la classe Dauphin, la construction est la suivante.
Dauphin::Dauphin(char * n,int x,int y,int p,int v)
: Poisson(n,x,y,p), Mammifere(n,x,y,v) {}
Le problème de l'héritage multiple devient alors flagrant: une instance
Dauphin se retrouve avec deux fois la partie Animal (l'une issue de
Poisson et l'autre de Mammifere), ce qui n'est pas du tout ce qu'on
souhaite ici.
Idéalement, il faudrait qu'un dauphin soit constitué d'une seule partie
Animal, d'une sous-partie Poisson et d'une sous-partie
Mammifere. Pour résoudre ce problème, il existe l'héritage
virtuel. L'idée est qu'on hérite d'une classe, mais si par héritage
multiple, on se retrouve avec plusieurs fois la même classe, alors on
ne la considère qu'une seule fois.
Dans notre
exemple, il faut donc effectuer les modifications suivantes.
class Poisson : public virtual Animal { ... };
class Mammifere : public virtual Animal { ... };
class Dauphin : public Poisson, public Mammifere {
...
Dauphin(char * n,int x,int y,int p,int v)
: Animal(n,x,y), Poisson(n,x,y,p), Mammifere(n,x,y,v) {}
...
};
Au niveau de la construction, il faut alors détailler le chaînage de la classe de
base (i.e. la super-classe la plus haute dans la hiérarchie) jusqu'aux sous-classes
immédiates. Même si les arguments n, x, y sont
répétés pour le constructeur de Animal, de Poisson
et de Mammifere, ils ne sont réellement pris en compte que dans la classe de
base Animal. Ensuite, ils ne servent qu'à repérer les arguments
significatifs pour chaque sous-classe, i.e. p et v dans notre
exemple.
| |
| CLASSE ABSTRAITE ET INTERFACE |
| |
Nous présentons ici brièvement la notion de classe abstraite, qui peut être
associée à la notion d'interface, qui est une alternative intéressante pour
l'héritage multiple. Ce concept est notamment proposé dans le langage Java.
Une méthode virtuelle peut ne pas avoir de corps. En effet, si l'on reprend l'exemple de
la classe Animal, on peut souhaiter ne pas fournir de comportement par défaut
aux méthodes virtuelles. Une solution peut être de définir les méthodes
de la manière suivante.
virtual void deplacer(void) {}
virtual Animal * engendrer(void) {}
On peut donc créer une instance de la classe Animal qui n'a pas vraiment de
sens, des méthodes sans action pouvant être exécutées.
Il peut être intéressant de définir alors des méthodes virtuelles qui
n'ont pas de corps, ainsi elles ne pourront pas être exécutées directement. Ces
méthodes sont dites abstraites, à l'opposé des autres qui sont dites
concrètes. L'intérêt d'une méthode abstraite est double: elle
empêche tout d'abord la classe d'être instanciée, ensuite elle oblige les
sous-classes à proposer une implémentation.
Une classe abstraite est tout simplement une classe qui possède au moins une
méthode abstraite. L'exemple suivant rend la classe Animal abstraite, en
déclarant les méthodes virtuelles deplacer et engendrer
abstraites.
virtual void deplacer(void) = 0;
virtual Animal * engendrer(void) = 0;
Les méthodes, comme les fonctions, sont des pointeurs en C++. Pour déclarer une
méthode abstraite, il suffit de lui affecter le pointeur NULL. Lorsqu'une
classe est abstraite, il est impossible de créer une instance de cette classe, seule la
manipulation de pointeurs ou de références est possible. Prenons l'exemple suivant,
toutes les lignes sont autorisées exceptée la première.
Animal a("un animal",0,0);
Poisson p("Maurice",10,20,3);
Animal * pa = &p;
Animal & ra = p;
| |
| Interface et classe abstraite pure |
| |
Une classe abstraite qui ne possède que des méthodes abstraites (aucune
méthode concrète, virtuelle ou non, et aucun attribut) est dite abstraite
pure. Elle ne décrit en fait qu'une interface, i.e. un jeu de méthodes qu'il
faudra implémenter dans les sous-classes si l'on souhaite les instancier. La notion de
classe abstraite pure est très proche de la notion d'interface en Java. Ce langage
interdit en effet l'héritage multiple de classes concrètes ou abstraites grâce
à ce concept.
Une classe est formée de deux parties: une interface (qui décrit la partie
visible: prototypes des méthodes publiques) et une implémentation (qui
décrit la partie cachée: attributs et contenu des méthodes). Lors de la
spécialisation d'une classe, son interface et de son implémentation sont
héritées toutes les deux, comme le montre la figure suivante.
Dans le
cas d'un héritage multiple, les problèmes surviennent au niveau de l'héritage
des implémentations: méthodes avec le même nom mais un corps différent,
ou héritage double d'une classe qui implique une duplication de l'implémentation.
Pour éviter ces difficultés, Java empêche l'héritage multiple
d'implémentations en imposant l'héritage simple pour les classes et en autorisant
l'héritage multiple pour les interfaces.
Dans tous nos exemples, les attributs que nous avons déclarés sont propres aux
instances, c'est-à-dire que chaque objet construit possède son jeu d'attributs
qu'il est le seul à pouvoir modifier (à condition qu'ils soient encapsulés).
Il est possible de déclarer des attributs propres non pas aux objets mais à la classe
elle-même. Cela signifie qu'il n'existe qu'une seule instance de ces attributs dans tout le
programme et qu'ils sont partagés entre toutes les instances de la classe. Le mot-clé
static est utilisé pour déclarer de tels attributs. Ceux-ci sont
appelés attributs de classe en opposition aux attributs communs dits
d'instance. Dans notre exemple, nous pouvons imaginer un compteur du nombre d'instances de
la classe Animal dans un programme. Pour cela, nous effectuons les modifications
suivantes.
class Animal {
...
protected:
static int _nb_instance;
public:
static int getNbInstance(void) { return _nb_instance; }
Animal(char * n,int x,int y)
: _nom(n), _x(x), _y(y) { ++_nb_instance; }
virtual ~Animal(void) { --_nb_instance; }
...
};
Pour manipuler ces attributs de classe, il est naturellement possible de
créer des méthodes de classe, en utilisant de manière similaire le
mot-clé static. Dans l'exemple, le constructeur et le destructeur sont
utilisés pour mettre à jour le compteur. Ainsi, à tout moment, on
connaît le nombre exact d'instances de la classe Animal dans un programme. Pour
accéder à un membre de classe, on utilise l'opérateur :: de la
manière suivante.
std::cout << "Nombre animaux: " <<
Animal::getNbInstance();
En résumé, une méthode de classe est équivalente à une
fonction, car une instance de la classe n'est pas nécessaire pour appeler l'une de ses
méthodes statiques, même si cela reste possible comme le montre le code suivant.
Poisson p("Maurice",10,20,3);
...
std::cout << "Nombre animaux: " << p.getNbInstance();
La seule différence avec une fonction est qu'une méthode statique appartient
à sa classe et peut donc accéder à tous ses membres statiques,
cachés ou non. En ce qui concerne les attributs de classe, ils sont aussi
très proches des variables globales, et à leur image, ils doivent être
initialisés dans un fichier source .cpp et non pas dans un header
.hpp (ce qui entraînerait des complications au niveau des inclusions de
fichiers). Ainsi, dans un fichier .hpp, on aura le code suivant.
class Animal {
...
protected: static int _nb_instance;
...
};
Et dans le fichier .cpp associé, on retrouvera le code suivant, qui
initialise l'attribut de classe.
int Animal::_nb_instance = 0;
Le mécanisme RTTI (Run-Time Type Information) est très utile pour
déterminer la classe réelle d'un objet en cours d'exécution, lorsque celui-ci
est pointé ou référencé. Il s'agit du même type de contrôle
que celui effectué par l'opérateur de conversion dynamic_cast. Le
mot-clé typeid est utilisé pour retourner une structure de type
type_info (ne pas oublier d'inclure le fichier <typeinfo>).
Considérons le code suivant.
#include <typeinfo>
...
Poisson p("Maurice",10,20,3);
Mammifere m("Rantanplan",5,9,17);
Animal * pa = &p;
Animal * pb = &m;
...
std::cout << typeid(*pa).name();
La structure type_info contient des informations sur le type fourni à
typeid. Notamment, comme le montre l'exemple, il est possible d'afficher le nom du
type en question. Mais le plus intéressant est que les opérateurs == et
!= ont été surchargés pour le type type_info. Ainsi,
il est possible de vérifier si les pointeurs pa et pb pointent
effectivement sur le même type d'objet.
if (typeid(*pa)==typeid(*pb))
std::cout << "Ils sont de même type.";
else std::cout << "Ils ne sont pas de même type.";
L'opérateur typeid peut également s'appliquer sur un type, ce qui
permet de tester directement qu'un pointeur référence bien, par exemple, un poisson.
if (typeid(*pa)==typeid(Poisson))
std::cout << "C'est un poisson.";
else std::cout << "Ce n'est pas un poisson.";
Il y a tout de même un petit piège avec l'opérateur typeid.
Pour obtenir le type réel d'un objet, il faut fournir sa référence et non pas
son pointeur, comme le montre le tableau ci-dessous. En effet, le pointeur est
considéré comme un type à part et il n'y a donc pas de lien d'héritage
entre deux pointeurs, i.e. un pointeur Poisson * n'a aucun lien (pour
typeid) avec Animal *, alors qu'une référence Animal
& peut en fait être une référence sur un objet de type
Poisson. Considérons le code suivant.
Animal * pp = new Poisson("Maurice",10,20,3);
Animal & rp = *pp;
Le tableau qui suit montre les égalités possibles entre les types, selon qu'il
s'agisse de pointeurs ou de références.
| |
typeid(Animal) |
typeid(Poisson) |
typeid(Animal *) |
typeid(Poisson *) |
typeid(pp) |
!= |
!= |
== |
!= |
typeid(rp) |
!= |
== |
!= |
!= |
typeid(*pp) |
!= |
== |
!= |
!= |
typeid(&rp) |
!= |
!= |
== |
!= |
Gérer les erreurs dans un programme est toujours une chose très délicate.
La technique habituelle consiste à traiter localement le problème, comme dans
l'exemple suivant.
void deplacerPoisson(Animal * a) {
Poisson * p = dynamic_cast<Poisson *>(a);
if (p==0) {
std::cerr << "Erreur: ce n'est pas un poisson." << std::endl;
exit(1);
}
... // Le poisson se déplace.
}
Le premier défaut d'une telle approche est de forcer un couplage entre la fonction
et la manière d'afficher du programme. En effet, la fonction écrit ici l'erreur sur
le flux standard, mais on pourrait imaginer que la fonction est utilisée dans une
application graphique où le message d'erreur apparaît dans une boîte de
dialogue. Une solution pourrait consister à utiliser une fonction qui se charge d'afficher
l'erreur.
void afficherErreur(char * s) {
std::cerr << "Erreur: " << s << std::endl;
exit(1);
}
void deplacerPoisson(Animal * a) {
Poisson * p = dynamic_cast<Poisson *>(a);
if (p==0) afficherErreur("ce n'est pas un poisson.");
... // Le poisson se déplace.
}
L'autre défaut de l'approche est qu'il est difficile dans ce cas de reprendre le
programme après le traitement de l'erreur. On a choisit ici de sortir du programme, mais on
pourrait imaginer vouloir reprendre le cours du programme à un endroit bien précis.
De ces principaux défauts sont nées les exceptions qui permettent une plus grande
souplesse et une certaine factorisation dans la gestion des erreurs. Reprenons notre exemple.
void deplacerPoisson(Animal * a) {
Poisson * p = dynamic_cast<Poisson *>(a);
if (p==0) throw Chaine("ce n'est pas un poisson.");
... // Le poisson se déplace.
}
Lorsqu'une erreur est détectée, on choisit de la traiter localement ou de la
transmettre à la méthode (ou à la fonction) appelante. C'est le rôle du
mot-clé throw qui jette un objet, appelé une exception,
à la méthode appelante. Dans l'exemple, throw transmet un objet
Chaine créé à la volée en appelant explicitement son
constructeur. La méthode courante est terminée en détruisant ses
variables locales et ses arguments. Notez que toutes les allocations dynamiques de la
méthode doivent être traitées manuellement. Une fois la méthode
terminée, la main est rendue à la méthode appelante qui suspend alors son
exécution et tente de traiter l'erreur.
int main(void) {
Animal * a = new Mammifere("Rantanplan",5,9,17);
try {
deplacerPoisson(a);
...
}
catch(const Chaine & c) {
std::cerr << "Erreur: " << c << std::endl;
return 1;
}
catch(...) {
std::cerr << "Erreur inconnue." << std::endl;
return 2;
}
return 0;
}
Dans la méthode appelante, une partie de code est surveillée par un bloc
try. Lorsqu'une exception est levée dans cette zone, l'exécution de la
méthode est suspendue et reprend dans l'une des méthodes catch qui suit
le bloc try. Les méthodes sont testées dans leur ordre de
déclaration. Dès que le type de l'argument d'une méthode catch
correspond au type de l'exception levée, alors la méthode est exécutée.
Dans notre exemple, si une exception de type Chaine est levée, alors c'est la
première méthode catch qui est exécutée. En revanche, pour
tous les autres types d'exception, c'est la seconde méthode (qui prend en paramètre
n'importe quel type d'objet grâce au mot-clé ...) qui est lancée.
Notez que pour éviter toute recopie de l'exception, les méthodes catch
reçoivent des références.
Dans l'hypothèse où une méthode m susceptible de recevoir une
exception ne la gère pas (soit parce qu'aucun bloc try n'a été
établi, soit parce qu'aucune méthode catch ne correspond au type de
l'exception) alors l'exception est automatiquement transmise à la méthode qui a
appelé m. Ce processus se répète jusqu'à la fonction
main où l'erreur doit impérativement être traitée. Si ce
n'est pas le cas, l'exception est lancée dans le vide et une erreur du genre aborted
apparaît.
Dans notre exemple, nous avons simplement jeté un objet de type Chaine. Mais
grâce à la STL, le C++ fournit une hiérarchie de classes d'exception. La
classe de base (i.e. la classe en haut de la hiérarchie) est exception. Elle se
décline ensuite en sous-classes pour divers types d'erreur: fichiers, débordement...
(cf. cette page pour les détails). Voici un exemple simple qui permet de
créer sa propre classe d'exception (très important pour éviter de
réécrire le message d'erreur à chaque appel, mais également pour
permettre des traitements différents au niveau des catch, en fonction de la
classe de l'exception).
#include <stdexcept>
class ErreurConversionPoisson : public std::exception {
public:
const char * what(void) const throw()
{ return "ce n'est pas un poisson."; }
};
void deplacerPoisson(Animal * a) {
Poisson * p = dynamic_cast<Poisson *>(a);
if (p==0) throw ErreurConversionPoisson();
... // Le poisson se déplace.
}
int main(void) {
Animal * a = new Mammifere("Rantanplan",5,9,17);
try {
deplacerPoisson(a);
...
}
catch(const std::exception & e) {
std::cerr << "Erreur: " << e.what() << std::endl;
return 1;
}
catch(...) {
std::cerr << "Erreur inconnue." << std::endl;
return 2;
}
return 0;
}
La classe exception implémente une méthode virtuelle
what qui retourne le message associé à l'erreur. Pour créer sa
propre classe d'exception, il suffit de redéfinir cette méthode avec son propre
message (ne pas oublier d'inclure le fichier <stdexcept>). Notez qu'il est tout
à fait possible de rajouter des attributs: par exemple, pour une erreur
d'entrée/sortie, le nom du fichier qui pose problème peut être
mémorisé.
class ErreurFichier : public std::exception {
protected:
Chaine _fichier;
public:
ErreurFichier(char * s) : _fichier(s) {}
const char * what(void) const throw() {
Chaine c = Chaine("impossible d'ouvrir le fichier
")+_fichier;
return (const char *)c;
}
};
...
if (...) throw ErreurFichier("dummy.txt");
...
| |
| |
| Copyright (c) 1999-2016 - Bruno Bachelet - bruno@nawouak.net -
http://www.nawouak.net |
| La permission est accordée de copier, distribuer et/ou modifier ce document
sous les termes de la licence GNU Free Documentation License, Version 1.1 ou toute
version ultérieure publiée par la fondation Free Software Foundation. Voir
cette licence pour plus de détails (http://www.gnu.org). |
|
|