• Aucun résultat trouvé

Exceptions dans les constructeurs et les destructeursdestructeurs

Chapitre 8. Les exceptions en C++

8.7. Exceptions dans les constructeurs et les destructeursdestructeurs

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. En revanche, les données membres et les classes de bases déjà contruites sont automatiquement détruites avant de remonter l’exception. De ce fait, toutes les ressources que le constructeur a com-mencé à réserver sont libérées automatiquement, pourvu qu’elles soient encapsulées dans des classes disposant de destructeurs.

Pour les autres ressources (ressources systèmes ou pointeurs contenant des données allouées dynami-quement), aucun code de destruction n’est exécuté. Cela peut conduire à des fuites de mémoire ou à une consommation de ressource qui ne pourront plus être libérées. De ce fait, il est conseillé de ne jamais faire de traitements complexes ou susceptibles de lancer une exception dans un constructeur, et de définir une fonction d’initialisation que l’on appelera dans un contexte plus fiable. Si cela n’est pas possible, il est impératif d’encapsuler toutes les ressources que le constructeur réservera dans des classes disposant d’un destructeur afin de libérer correctement la mémoire.

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érateurdeleteafin 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érateurdelete manuellement.

Note :Le compilateur détruit les données membres et les classes de base qui ont été construites avant le lancement de l’exception avant d’appeler l’opérateur delete pour libérer la mémoire.

D’ordinaire, c’est cet opérateur qui appelle le destructeur de l’objet à détruire, mais il ne le fait pas une deuxième fois. Le comportement de l’opérateurdeleteest donc lui aussi légèrement modifié par le mécanisme des exceptions lorsqu’il s’en produit une pendant la construction d’un objet.

Il est possible de capter les exceptions qui se produisent pendant l’exécution du constructeur d’une classe, afin de faire un traitement sur cette exception, ou éventuellement afin de la traduire dans une autre exception. Pour cela, le C++ fournit une syntaxe particulière. Cette syntaxe permet simplement d’utiliser un bloctrypour le corps de fonction des constructeurs. Les blocscatchsuivent alors la définition du constructeur, et effectuent les traitements sur l’exception.

Exemple 8-7. Exceptions dans les constructeurs

#include <stdlib.h>

#include <iostream>

using namespace std;

Chapitre 8. Les exceptions en C++

Buffer pBuffer; // Pointeur à libération automatique.

int *pData; // Pointeur classique.

public:

A() throw (double);

~A();

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 (double)

try : pData() {

cout << "Début du constructeur" << endl;

pBuffer.p = new char[10];

pData = new int[10]; // Fuite de mémoire ! cout << "Lancement de l’exception" << endl;

throw 2;

}

catch (int) {

cout << "catch du constructeur..." << endl;

// delete[] pData; // Illégal! L’objet est déjà détruit ! // Conversion de l’exception en flottant :

throw 3.14;

} A::~A() {

Chapitre 8. Les exceptions en C++

// Libération des données non automatiques : delete[] pData; // Jamais appelé ! cout << "A::~A()" << endl;

}

cout << "Aïe, même pas mal !" << endl;

}

return EXIT_SUCCESS;

}

Dans cet exemple, lors de la création dynamique d’un objet A, une erreur d’initialisation se produit et une exception de type entier est lancée. Celle-ci est alors traitée dans le bloccatch qui suit la définition du constructeur de la classe A. Ce bloc convertit l’exception en exception de type flottant.

L’opérateurdeleteest bien appelé automatiquement, mais le destructeur de A n’est jamais exécuté.

Comme indiqué dans le bloc catchdu constructeur, les données membres de l’objet, qui est déjà détruit, ne sont plus accessibles. De ce fait, le bloc alloué dynamiquement pourpDatane peut être libéré, et comme le destructeur n’est pas appelé, ce bloc de mémoire est définitivement perdu. On ne peut corriger le problème que de deux manières : soit on n’effectue pas ce type de traitement dans le constructeur, soit on encapsule le pointeur dans une classe disposant d’un destructeur. L’exemple pré-cédent définit une classe Buffer pour la donnée membrepBuffer. Pour information, la bibliothèque standard C++ fournit une classe générique similaire que l’on pourra également utiliser dans ce genre de situation (voir la Section 14.2.1 pour plus de détails à ce sujet).

Le comportement du bloc catchdes constructeurs avec bloc try est différent de celui des blocs catchclassiques. L’objet n’ayant pu être construit, l’exception doit obligatoirement être transmise au code qui a cherché à le créer, afin d’éviter de le laisser dans un état incohérent (c’est-à-dire avec une référence d’objet non construit). Par conséquent, contrairement aux exceptions traitées dans un bloccatchclassique, les exceptions lancées dans les constructeurs sont automatiquement relancées une fois qu’elles ont été traitées dans le bloccatchdu constructeur. L’exception doit donc toujours être captée dans le code qui cherche à créer l’objet, afin de prendre les mesures adéquates. Ainsi, dans l’exemple précédent, si l’exception n’était pas convertie en exception de type flottant, le programme planterait, car elle serait malgré tout relancée automatiquement et la fonctionmainne dispose pas de bloccatchpour les exceptions de type entier.

Note :Cette 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 bloccatchla relancera. On évitera donc à tout prix de créer des objets globaux dont les constructeurs effectuent des tâches susceptibles de lancer une exception.

De manière générale, comme il l’a déjà été dit, il n’est de toutes manières pas sage de réaliser des tâches complexes dans un constructeur et lors de l’initialisation des données statiques.

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étryet la première accolade. En effet, les constructeurs

Chapitre 8. Les exceptions en C++

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 ...

Enfin, les exceptions lancées dans les destructeurs ne peuvent être captées. En effet, elles corres-pondent à une situation grave, puisqu’il n’est pas pas possible de détruire correctement l’objet dans ce cas. Dans ce cas, la fonctionstd::terminateest appelée automatiquement, et le programme se termine en conséquence.

Chapitre 9. Identification dynamique des