9. Les exceptions en C++▲
Une exception est l'interruption de l'exécution du programme à la suite d'un événement particulier. Le but des exceptions est de réaliser des traitements spécifiques aux événements qui en sont la cause. Ces traitements peuvent rétablir le programme dans son mode de fonctionnement normal, auquel cas son exécution reprend. Il se peut aussi que le programme se termine, si aucun traitement n'est approprié.
Le C++ supporte les exceptions logicielles, dont le but est de gérer les erreurs qui surviennent lors de l'exécution des programmes. Lorsqu'une telle erreur survient, le programme doit lancer une exception. L'exécution normale du programme s'arrête dès que l'exception est lancée, et le contrôle est passé à un gestionnaire d'exception. Lorsqu'un gestionnaire d'exception s'exécute, on dit qu'il a attrapé l'exception.
Les exceptions permettent une gestion simplifiée des erreurs, parce qu'elles en reportent le traitement. Le code peut alors être écrit sans se soucier des cas particuliers, ce qui le simplifie grandement. Les cas particuliers sont traités dans les gestionnaires d'exception.
En général, une fonction qui détecte une erreur d'exécution ne peut pas se terminer normalement. Comme son traitement n'a pas pu se dérouler normalement, il est probable que la fonction qui l'a appelée considère elle aussi qu'une erreur a eu lieu et termine son exécution. L'erreur remonte ainsi la liste des appelants de la fonction qui a généré l'erreur. Ce processus continue, de fonction en fonction, jusqu'à ce que l'erreur soit complètement gérée ou jusqu'à ce que le programme se termine (ce cas survient lorsque la fonction principale ne peut pas gérer l'erreur).
Traditionnellement, ce mécanisme est implémenté à l'aide de codes de retour des fonctions. Chaque fonction doit renvoyer une valeur spécifique à l'issue de son exécution, permettant d'indiquer si elle s'est correctement déroulée ou non. La valeur renvoyée est donc utilisée par l'appelant pour déterminer la nature de l'erreur, et, si erreur il y a, prendre les mesures nécessaires. Cette méthode permet à chaque fonction de libérer les ressources qu'elle a allouées lors de la remontée des erreurs, et d'effectuer ainsi sa part du traitement d'erreur.
Malheureusement, cette technique nécessite de tester les codes de retour de chaque fonction appelée, et la logique d'erreur développée finit par devenir très lourde, puisque ces tests s'imbriquent les uns à la suite des autres et que le code du traitement des erreurs se trouve mélangé avec le code du fonctionnement normal de l'algorithme. Cette complication peut devenir ingérable lorsque plusieurs valeurs de codes de retour peuvent être renvoyées afin de distinguer les différents cas d'erreur possible, car il peut en découler un grand nombre de tests et beaucoup de cas particuliers à gérer dans les fonctions appelantes.
Certains programmes utilisent donc une solution astucieuse, qui consiste à déporter le traitement des erreurs à effectuer en dehors de l'algorithme par des sauts vers la fin de la fonction. Le code de nettoyage, qui se trouve alors après l'algorithme, est exécuté complètement si tout se passe correctement. En revanche, si la moindre erreur est détectée en cours d'exécution, un saut est réalisé vers la partie du code de nettoyage correspondante au traitement qui a déjà été effectué. Ainsi, ce code n'est écrit qu'une seule fois, et le traitement des erreurs est situé en dehors du traitement normal.
La solution précédente est tout à fait valable (en fait, c'est même la solution la plus simple), mais elle souffre d'un inconvénient. Elle rend le programme moins structuré, car toutes les ressources utilisées par l'algorithme doivent être accessibles depuis le code de traitement des erreurs. Ces ressources doivent donc être placées dans une portée relativement globale, voire déclarées en tête de fonction. De plus, le traitement des codes d'erreurs multiples pose toujours les mêmes problèmes de complication des tests.
La solution qui met en oeuvre les exceptions est beaucoup plus simple, puisque la fonction qui détecte une erreur peut se contenter de lancer une exception. Cette exception interrompt l'exécution de la fonction, et un gestionnaire d'exception approprié est recherché. La recherche du gestionnaire suit le même chemin que celui utilisé lors de la remontée des erreurs : à savoir la liste des appelants. La première fonction appelante qui contient un gestionnaire d'exception approprié prend donc le contrôle, et effectue le traitement de l'erreur. Si le traitement est complet, le programme reprend son exécution normale. Dans le cas contraire, le gestionnaire d'exception peut relancer l'exception (auquel cas le gestionnaire d'exception suivant est recherché) ou terminer le programme.
Le mécanisme des exceptions du C++ garantit que tous les objets de classe de stockage automatique sont détruits lorsque l'exception qui remonte sort de leur portée. Ainsi, si toutes les ressources sont encapsulées dans des classes disposant d'un destructeur capable de les détruire ou de les ramener dans un état cohérent, la remontée des exceptions effectue automatiquement le ménage. De plus, les exceptions peuvent être typées, et caractériser ainsi la nature de l'erreur qui s'est produite. Ce mécanisme est donc strictement équivalent en termes de fonctionnalités aux codes d'erreurs utilisés précédemment.
Comme on le voit, les exceptions permettent de simplifier le code, en reportant en dehors de l'algorithme normal le traitement des erreurs. Par ailleurs, la logique d'erreur est complètement prise en charge par le langage, et le programmeur n'a plus à faire les tests qui permettent de déterminer le traitement approprié pour chaque type d'erreur. Les mécanismes de gestion des exceptions du C++ sont décrits dans les paragraphes suivants.
9.1. Lancement et récupération d'une exception▲
En C++, lorsqu'il faut lancer une exception, on doit créer un objet dont la classe caractérise cette exception, et utiliser le mot clé throw. Sa syntaxe est la suivante :
throw
objet;
où objet est l'objet correspondant à l'exception. Cet objet peut être de n'importe quel type, et pourra ainsi caractériser pleinement l'exception.
L'exception doit alors être traitée par le gestionnaire d'exception correspondant. On ne peut attraper que les exceptions qui sont apparues dans une zone de code limitée (cette zone est dite protégée contre les erreurs d'exécution), pas sur tout un programme. On doit donc placer le code susceptible de lancer une exception d'un bloc d'instructions particulier. Ce bloc est introduit avec le mot clé try :
try
{
// Code susceptible de générer des exceptions...
}
Les gestionnaires d'exceptions doivent suivre le bloc try. Ils sont introduits avec le mot clé catch :
catch
(classe [&
][temp])
{
// Traitement de l'exception associée à la classe
}
Notez que les objets de classe de stockage automatique définis dans le bloc try sont automatiquement détruits lorsqu'une exception fait sortir le contrôle du programme de leur portée. C'est également le cas de l'objet construit pour lancer l'exception. Le compilateur effectue donc une copie de cet objet pour le transférer au premier bloc catch capable de le recevoir. Cela implique qu'il y ait un constructeur de copie pour les classes d'exceptions non triviales.
De même, les blocs catch peuvent recevoir leurs paramètres par valeur ou par référence, comme le montre la syntaxe indiquée ci-dessus. En général, il est préférable d'utiliser une référence, afin d'éviter une nouvelle copie de l'objet de l'exception pour le bloc catch. Toutefois, on prendra garde au fait que dans ce cas, les modifications effectuées sur le paramètre seront effectuées dans la copie de travail du compilateur et seront donc également visibles dans les blocs catch des fonctions appelantes ou de portée supérieure, si l'exception est relancée après traitement.
Il peut y avoir plusieurs gestionnaires d'exceptions. Chacun traitera les exceptions qui ont été générées dans le bloc try et dont l'objet est de la classe indiquée par son paramètre. Il n'est pas nécessaire de donner un nom à l'objet (temp) dans l'expression catch. Cependant, cela permet de le récupérer, ce qui peut être nécessaire si l'on doit récupérer des informations sur la nature de l'erreur.
Enfin, il est possible de définir un gestionnaire d'exception universel, qui récupérera toutes les exceptions possibles, quels que soient leurs types. Ce gestionnaire d'exception doit prendre comme paramètre trois points de suspension entre parenthèses dans sa clause catch. Bien entendu, dans ce cas, il est impossible de spécifier une variable qui contient l'exception, puisque son type est indéfini.
#include
<iostream>
using
namespace
std;
class
erreur // Première exception possible, associée
// à l'objet erreur.
{
public
:
int
cause; // Entier spécifiant la cause de l'exception.
// Le constructeur. Il appelle le constructeur de cause.
erreur(int
c) : cause(c) {}
// Le constructeur de copie. Il est utilisé par le mécanisme
// des exceptions :
erreur(const
erreur &
source) : cause(source.cause) {}
}
;
class
other {}
; // Objet correspondant à toutes
// les autres exceptions.
int
main(void
)
{
int
i; // Type de l'exception à générer.
cout <<
"Tapez 0 pour générer une exception Erreur, "
"1 pour une Entière :"
;
cin >>
i; // On va générer une des trois exceptions
// possibles.
cout <<
endl;
try
// Bloc où les exceptions sont prises en charge.
{
switch
(i) // Selon le type d'exception désirée,
{
case
0
:
{
erreur a(0
);
throw
(a); // on lance l'objet correspondant
// (ici, de classe erreur).
// Cela interrompt le code. break est
// donc inutile ici.
}
case
1
:
{
int
a=
1
;
throw
(a); // Exception de type entier.
}
default
:
// Si l'utilisateur n'a pas tapé 0 ou 1,
{
other c; // on crée l'objet c (type d'exception
throw
(c); // other) et on le lance.
}
}
}
// fin du bloc try. Les blocs catch suivent :
catch
(erreur &
tmp) // Traitement de l'exception erreur ...
{
// (avec récupération de la cause).
cout <<
"Erreur erreur ! (cause "
<<
tmp.cause <<
")"
<<
endl;
}
catch
(int
tmp) // Traitement de l'exception int...
{
cout <<
"Erreur int ! (cause "
<<
tmp <<
")"
<<
endl;
}
catch
(...) // Traitement de toutes les autres
{
// exceptions (...).
// On ne peut pas récupérer l'objet ici.
cout <<
"Exception inattendue !"
<<
endl;
}
return
0
;
}
Selon ce qu'entre l'utilisateur, une exception du type erreur, int ou other est générée.
9.2. Remontée des exceptions▲
Les fonctions intéressées par les exceptions doivent les capter avec le mot clé catch comme on l'a vu ci-dessus. Elles peuvent alors effectuer tous les traitements d'erreurs que le C++ ne fera pas automatiquement. Ces traitements comprennent généralement le rétablissement de l'état des données manipulées par la fonction (dont, pour les fonctions membres d'une classe, les données membres de l'objet courant), ainsi que la libération des ressources non encapsulées dans des objets de classe de stockage automatique (par exemple, les fichiers ouverts, les connexions réseau, etc.).
Une fois ce travail effectué, elles peuvent, si elles le désirent, relancer l'exception, afin de permettre un traitement complémentaire par leur fonction appelante. Le parcours de l'exception s'arrêtera donc dès que l'erreur aura été complètement traitée. Bien entendu, il est également possible de lancer une autre exception que celle que l'on a reçue, comme ce peut être par exemple le cas si le traitement de l'erreur provoque lui-même une erreur.
Pour relancer l'exception en cours de traitement dans un gestionnaire d'exception, il faut utiliser le mot clé throw. La syntaxe est la suivante :
throw
;
L'exception est alors relancée, avec comme valeur l'objet que le compilateur a construit en interne pour propager l'exception. Les gestionnaires d'exception peuvent donc modifier les paramètres des exceptions, s'ils les attrapent avec une référence.
Si, lorsqu'une exception se produit dans un bloc try, il est impossible de trouver le bloc catch correspondant à la classe de cette exception, il se produit une erreur d'exécution. La fonction prédéfinie std::terminate est alors appelée. Elle se contente d'appeler une fonction de traitement de l'erreur, qui elle-même appelle la fonction abort de la bibliothèque C. Cette fonction termine en catastrophe l'exécution du programme fautif en générant une faute (les ressources allouées par le programme ne sont donc pas libérées, et des données peuvent être perdues). Ce n'est généralement pas le comportement désiré, aussi est-il possible de le modifier en changeant la fonction appelée par std::terminate.
Pour cela, il faut utiliser la fonction std::set_terminate, qui attend en paramètre un pointeur sur la fonction de traitement d'erreur, qui ne prend aucun paramètre et renvoie void. La valeur renvoyée par std::set_terminate est le pointeur sur la fonction de traitement d'erreur précédente. std::terminate et std::set_terminate sont déclaréee dans le fichier d'en-tête exception.
Note : Comme leurs noms l'indiquent, std::terminate et std::set_terminate sont déclarées dans l'espace de nommage std::, qui est réservé pour tous les objets de la bibliothèque standard C++. Si vous ne voulez pas à avoir à utiliser systématiquement le préfixe std:: devant ces noms, vous devrez ajouter la ligne « using namespace std; » après avoir inclus l'en-tête exception. Vous obtiendrez de plus amples renseignements sur les espaces de nommage dans le Chapitre 11.
#include
<iostream>
#include
<exception>
using
namespace
std;
void
mon_gestionnaire(void
)
{
cout <<
"Exception non gérée reçue !"
<<
endl;
cout <<
"Je termine le programme proprement..."
<<
endl;
exit(-
1
);
}
int
lance_exception(void
)
{
throw
2
;
}
int
main(void
)
{
set_terminate(&
mon_gestionnaire);
try
{
lance_exception();
}
catch
(double
d)
{
cout <<
"Exception de type double reçue : "
<<
d <<
endl;
}
return
0
;
}
9.3. Liste des exceptions autorisées pour une fonction▲
Il est possible de spécifier les exceptions qui peuvent être lancées par une fonction. Pour cela, il faut faire suivre son en-tête du mot clé throw avec, entre parenthèses et séparées par des virgules, les classes des exceptions qu'elle est autorisée à lancer. Par exemple, la fonction suivante :
int
fonction_sensible(void
)
throw
(int
, double
, erreur)
{
...
}
n'a le droit de lancer que des exceptions du type int, double ou erreur. Si une exception d'un autre type est lancée, par exemple une exception du type char *, il se produit encore une fois une erreur à l'exécution.
En fait, la fonction std::unexpected est appelée. Cette fonction se comporte de manière similaire à std::terminate, puisqu'elle appelle par défaut une fonction de traitement de l'erreur qui elle-même appelle la fonction std::terminate (et donc abort en fin de compte). Cela conduit à la terminaison du programme. On peut encore une fois changer ce comportement par défaut en remplaçant la fonction appelée par std::unexpected par une autre fonction à l'aide de std::set_unexpected, qui est déclarée dans le fichier d'en-tête exception. Cette dernière attend en paramètre un pointeur sur la fonction de traitement d'erreur, qui ne doit prendre aucun paramètre et qui ne doit rien renvoyer. std::set_unexpected renvoie le pointeur sur la fonction de traitement d'erreur précédemment appelée par std::unexpected.
Note : Comme leurs noms l'indiquent, std::unexpected et std::set_unexpected sont déclarées dans l'espace de nommage std::, qui est réservé pour les objets de la bibliothèque standard C++. Si vous ne voulez pas avoir à utiliser systématiquement le préfixe std:: pour ces noms, vous devrez ajouter la ligne « using namespace std; » après avoir inclus l'en-tête exception. Vous obtiendrez de plus amples renseignements sur les espaces de nommage dans le Chapitre 11.
Il est possible de relancer une autre exception à l'intérieur de la fonction de traitement d'erreur. Si cette exception satisfait la liste des exceptions autorisées, le programme reprend son cours normalement dans le gestionnaire correspondant. C'est généralement ce que l'on cherche à faire. Le gestionnaire peut également lancer une exception de type std::bad_exception, déclarée comme suit dans le fichier d'en-tête exception :
class
bad_exception : public
exception
{
public
:
bad_exception(void
) throw
();
bad_exception(const
bad_exception &
) throw
();
bad_exception &
operator
=
(const
bad_exception &
) throw
();
virtual
~
bad_exception(void
) throw
();
virtual
const
char
*
what(void
) const
throw
();
}
;
Cela a pour conséquence de terminer le programme.
Enfin, le gestionnaire d'exceptions non autorisées peut directement mettre fin à l'exécution du programme en appelant std::terminate. C'est le comportement utilisé par la fonction std::unexpected définie par défaut.
#include
<iostream>
#include
<exception>
using
namespace
std;
void
mon_gestionnaire(void
)
{
cout <<
"Une exception illégale a été lancée."
<<
endl;
cout <<
"Je relance une exception de type int."
<<
endl;
throw
2
;
}
int
f(void
) throw
(int
)
{
throw
"5.35"
;
}
int
main(void
)
{
set_unexpected(&
mon_gestionnaire);
try
{
f();
}
catch
(int
i)
{
cout <<
"Exception de type int reçue : "
<<
i <<
endl;
}
return
0
;
}
Note : La liste des exceptions autorisées dans une fonction ne fait pas partie de sa signature. Elle n'intervient donc pas dans les mécanismes de surcharge des fonctions. De plus, elle doit se placer après le mot clé const dans les déclarations de fonctions membres const (en revanche, elle doit se placer avant =0 dans les déclarations des fonctions virtuelles pures).
On prendra garde au fait que les exceptions ne sont pas générées par le mécanisme de gestion des erreurs du C++ (ni du C). Cela signifie que pour avoir une exception, il faut la lancer, le compilateur ne fera pas les tests pour vous (tests de débordements numériques dans les calculs par exemple). Cela supposerait de prédéfinir un ensemble de classes pour les erreurs génériques. Les tests de validité d'une opération doivent donc être faits malgré tout et, le cas échéant, il faut lancer une exception pour reporter le traitement en cas d'échec. De même, les exceptions générées par la machine hôte du programme ne sont en général pas récupérées par les implémentations et, si elles le sont, les programmes qui les utilisent ne sont pas portables.
9.4. Hiérarchie des exceptions▲
Le mécanisme des exceptions du C++ se base sur le typage des objets, puisque le lancement d'une exception nécessite la construction d'un objet qui la caractérise, et le bloc catch destination de cette exception sera sélectionné en fonction du type de cet objet. Bien entendu, les objets utilisés pour lancer les exceptions peuvent contenir des informations concernant la nature des erreurs qui se produisent, mais il est également possible de classifier ces erreurs par catégories en se basant sur leurs types.
En effet, les objets exceptions peuvent être des instances de classes disposant de relations d'héritage. Comme les objets des classes dérivées peuvent être considérés comme des instances de leurs classes de base, les gestionnaires d'exception peuvent récupérer les exceptions de ces classes dérivées en récupérant un objet du type d'une de leurs classes de base. Ainsi, il est possible de classifier les différents cas d'erreurs en définissant une hiérarchie de classe d'exceptions, et d'écrire des traitements génériques en n'utilisant que les objets d'un certain niveau dans cette hiérarchie.
Le mécanisme des exceptions se montre donc plus puissant que toutes les autres méthodes de traitement d'erreurs à ce niveau, puisque la sélection du gestionnaire d'erreur est automatiquement réalisée par le langage. Cela peut être très pratique pour peu que l'on ait défini correctement sa hiérarchie de classes d'exceptions.
#include
<iostream>
using
namespace
std;
// Classe de base de toutes les exceptions :
class
ExRuntimeError
{
}
;
// Classe de base des exceptions pouvant se produire
// lors de manipulations de fichiers :
class
ExFileError : public
ExRuntimeError
{
}
;
// Classes des erreurs de manipulation des fichiers :
class
ExInvalidName : public
ExFileError
{
}
;
class
ExEndOfFile : public
ExFileError
{
}
;
class
ExNoSpace : public
ExFileError
{
}
;
class
ExMediumFull : public
ExNoSpace
{
}
;
class
ExFileSizeMaxLimit : public
ExNoSpace
{
}
;
// Fonction faisant un travail quelconque sur un fichier :
void
WriteData(const
char
*
szFileName)
{
// Exemple d'erreur :
if
(szFileName ==
NULL
) throw
ExInvalidName();
else
{
// Traitement de la fonction
// etc.
// Lancement d'une exception :
throw
ExMediumFull();
}
}
void
Save(const
char
*
szFileName)
{
try
{
WriteData(szFileName);
}
// Traitement d'un erreur spécifique :
catch
(ExInvalidName &
)
{
cout <<
"Impossible de faire la sauvegarde"
<<
endl;
}
// Traitement de toutes les autres erreurs en groupe :
catch
(ExFileError &
)
{
cout <<
"Erreur d'entrée / sortie"
<<
endl;
}
}
int
main(void
)
{
Save(NULL
);
Save("data.dat"
);
return
0
;
}
La bibliothèque standard C++ définit elle-même un certain nombre d'exceptions standards, qui sont utilisées pour signaler les erreurs qui se produisent à l'exécution des programmes. Quelques-unes de ces exceptions ont déjà été présentées avec les fonctionnalités qui sont susceptibles de les lancer. Vous trouverez une liste complète des exceptions de la bibliothèque standard du C++ dans la Section 13.2.
9.5. Exceptions dans les constructeurs▲
Il est parfaitement légal de lancer une exception dans un constructeur. En fait, c'est même la seule solution pour signaler une erreur lors de la construction d'un objet, puisque les constructeurs n'ont pas de valeur de retour.
Lorsqu'une exception est lancée à partir d'un constructeur, la construction de l'objet échoue. Par conséquent, le compilateur n'appellera jamais le destructeur pour cet objet, puisque cela n'a pas de sens. Cependant, ce comportement soulève le problème des objets partiellement initialisés, pour lesquels il est nécessaire de faire un peu de nettoyage à la suite du lancement de l'exception. Le C++ dispose donc d'une syntaxe particulière pour les constructeurs des objets susceptibles de lancer des exceptions. Cette syntaxe permet simplement d'utiliser un bloc try pour le corps de fonction des constructeurs. Les blocs catch suivent alors la définition du constructeur, et effectuent la libération des ressources que le constructeur aurait pu allouer avant que l'exception ne se produise.
Le comportement du bloc catch des constructeurs avec bloc try est différent de celui des blocs catch classiques. En effet, les exceptions ne sont normalement pas relancées une fois qu'elles ont été traitées. Comme on l'a vu ci-dessus, il faut utiliser explicitement le mot clé throw pour relancer une exception à l'issue de son traitement. Dans le cas des constructeurs avec un bloc try cependant, l'exception est systématiquement relancée. Le bloc catch du constructeur ne doit donc prendre en charge que la destruction des données membres partiellement construites, et il faut toujours capter l'exception au niveau du programme qui a cherché à créer l'objet.
Note : Cette dernière règle implique que les programmes déclarant des objets globaux dont le constructeur peut lancer une exception risquent de se terminer en catastrophe. En effet, si une exception est lancée par ce constructeur à l'initialisation du programme, aucun gestionnaire d'exception ne sera en mesure de la capter lorsque le bloc catch la relancera.
De même, lorsque la construction de l'objet se fait dans le cadre d'une allocation dynamique de mémoire, le compilateur appelle automatiquement l'opérateur delete afin de restituer la mémoire allouée pour cet objet. Il est donc inutile de restituer la mémoire de l'objet alloué dans le traitement de l'exception qui suit la création dynamique de l'objet, et il ne faut pas y appeler l'opérateur delete manuellement.
Note : Comme il l'a été dit plus haut, le compilateur n'appelle pas le destructeur pour les objets dont le constructeur a généré une exception. Cette règle est valide même dans le cas des objets alloués dynamiquement. Le comportement de l'opérateur delete est donc lui aussi légèrement modifié par le fait que l'exception s'est produite dans un constructeur.
#include
<iostream>
#include
<stdlib.h>
using
namespace
std;
class
A
{
char
*
pBuffer;
int
*
pData;
public
:
A() throw
(int
);
~
A()
{
cout <<
"A::~A()"
<<
endl;
}
static
void
*
operator
new
(size_t taille)
{
cout <<
"new()"
<<
endl;
return
malloc(taille);
}
static
void
operator
delete
(void
*
p)
{
cout <<
"delete"
<<
endl;
free(p);
}
}
;
// Constructeur susceptible de lancer une exception :
A::
A() throw
(int
)
try
{
pBuffer =
NULL
;
pData =
NULL
;
cout <<
"Début du constructeur"
<<
endl;
pBuffer =
new
char
[256
];
cout <<
"Lancement de l'exception"
<<
endl;
throw
2
;
// Code inaccessible :
pData =
new
int
;
}
catch
(int
)
{
cout <<
"Je fais le ménage..."
<<
endl;
delete
[] pBuffer;
delete
pData;
}
int
main(void
)
{
try
{
A *
a =
new
A;
}
catch
(...)
{
cout <<
"Aïe, même pas mal !"
<<
endl;
}
return
0
;
}
Dans cet exemple, lors de la création dynamique d'un objet A, une erreur d'initialisation se produit et une exception est lancée. Celle-ci est alors traitée dans le bloc catch qui suit la définition du constructeur de la classe A. L'opérateur delete est bien appelé automatiquement, mais le destructeur de A n'est jamais exécuté.
En général, si une classe hérite de une ou plusieurs classes de base, l'appel aux constructeurs des classes de base doit se faire entre le mot clé try et la première accolade. En effet, les constructeurs des classes de base sont susceptibles, eux aussi, de lancer des exceptions. La syntaxe est alors la suivante :
Classe::
Classe
try
: Base(paramètres) [, Base(paramètres) [...]]
{
}
catch
...