• Aucun résultat trouvé

Exceptions dans les constructeurs

I. Le langage C++

9. Les exceptions en C++

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

Chapitre 9. Les exceptions en C++

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.

Exemple 9-5. Exceptions dans les constructeurs

#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 destruc- teur 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 ...

Chapitre 10. Identification dynamique des

types

Le C++ est un langage fortement typé. Malgré cela, il se peut que le type exact d'un objet soit in- connu à cause de l'héritage. Par exemple, si un objet est considéré comme un objet d'une classe de base de sa véritable classe, on ne peut pas déterminer a priori quelle est sa véritable nature.

Cependant, les objets polymorphiques (qui, rappelons-le, sont des objets disposant de méthodes virtuelles) conservent des informations sur leur type dynamique, à savoir leur véritable nature. En effet, lors de l'appel des méthodes virtuelles, la méthode appelée est la méthode de la véritable classe de l'objet.

Il est possible d'utiliser cette propriété pour mettre en place un mécanisme permettant d'identifier le type dynamique des objets, mais cette manière de procéder n'est pas portable. Le C++ fournit donc un mécanisme standard permettant de manipuler les informations de type des objets polymor- phiques. Ce mécanisme prend en charge l'identification dynamique des types et la vérification de la

validité des transtypages dans le cadre de la dérivation.

10.1. Identification dynamique des types

10.1.1. L'opérateur typeid

Le C++ fournit l'opérateur typeid afin de récupérer les informations de type des expressions. Sa syntaxe est la suivante :

typeid(expression)

où expression est l'expression dont il faut déterminer le type.

Le résultat de l'opérateur typeid est une référence sur un objet constant de classe type_info. Cette classe sera décrite dans la Section 10.1.2.

Les informations de type récupérées sont les informations de type statique pour les types non po- lymorphiques. Cela signifie que l'objet renvoyé par typeid caractérisera le type de l'expression fournie en paramètre, que cette expression soit un sous-objet d'un objet plus dérivé ou non. En re- vanche, pour les types polymorphiques, si le type ne peut pas être déterminé statiquement (c'est-à-dire à la compilation), une détermination dynamique (c'est-à-dire à l'exécution) du type a lieu, et l'objet de classe type_info renvoyé décrit le vrai type de l'expression (même si elle représente un sous-objet d'un objet d'une classe dérivée). Cette situation peut arriver lorsqu'on manipule un ob- jet à l'aide d'un pointeur ou d'une référence sur une classe de base de la classe de cet objet.

Exemple 10-1. Opérateur typeid

#include <typeinfo> using namespace std; class Base

{ public:

virtual ~Base(void); // Il faut une fonction virtuelle // pour avoir du polymorphisme. };

Base::~Base(void) {

return ; }

class Derivee : public Base { public: virtual ~Derivee(void); }; Derivee::~Derivee(void) { return ; } int main(void) {

Derivee* pd = new Derivee; Base* pb = pd;

const type_info &t1=typeid(*pd); // t1 qualifie le type de *pd. const type_info &t2=typeid(*pb); // t2 qualifie le type de *pb. return 0 ;

}

Les objets t1 et t2 sont égaux, puisqu'ils qualifient tous les deux le même type (à savoir, la classe Derivee). t2 ne contient pas les informations de type de la classe Base, parce que le vrai type de l'objet pointé par pb est la classe Derivee.

Note : Notez que la classe type_info est définie dans l'espace de nommage std::, réservé à la librairie standard C++, dans l'en-tête typeinfo. Par conséquent, son nom doit être précédé du préfixe std::. Vous pouvez vous passer de ce préfixe en important les définitions de l'espace de nommage de la librairie standard à l'aide d'une directive using. Vous trouverez de plus amples renseignements sur les espaces de nommage dans le Chapitre 11.

On fera bien attention à déréférencer les pointeurs, car sinon, on obtient les informations de type sur ce pointeur, pas sur l'objet pointé. Si le pointeur déréférencé est le pointeur nul, l'opérateur ty- peid lance une exception dont l'objet est une instance de la classe bad_typeid. Cette classe est défi- nie comme suit dans l'en-tête typeinfo :

class bad_typeid : public logic {

public:

bad_typeid(const char * what_arg) : logic(what_arg) { return ; } void raise(void) { handle_raise(); throw *this; } };