• Aucun résultat trouvé

Chapitre 8. Les exceptions en C++

8.1. Techniques de gestion des erreurs

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éter-miner si l’appel s’est bien effectué ou non, et, si erreur il y a, prendre les mesures nécessaires. La nature de l’erreur peut être indiquée soit directement par la valeur retournée par la fonction, soit par une donnée globale que l’appelant peut consulter.

Exemple 8-1. Gestion des erreurs par codes d’erreur

// Fonction qui réserve trois ressources et effectue // un travail avec ces trois ressources.

// Retourne 0 si tout s’est bien passé,

// -1 si la première ressource ne peut être prise, // -2 si la deuxième ressource ne peut être prise, // -3 si la troisième ressource ne peut être prie, // et -1001 à -1004 selon le code d’erreur du travail.

// Les ressources sont consommées par le travail // si celui-ci réussit.

int fait_boulot(const char *rsrc1, const char *rsrc2, const char *rsrc3)

{

// Initilise le code de résultat à la première // erreur possible :

Chapitre 8. Les exceptions en C++

int res = -1;

// Alloue la première ressource : int r1 = alloue_ressource(rsrc1);

// Idem avec la troisième : res = -3;

int r3 = alloue_ressource(rsrc3);

if (r3 != 0) {

// OK, on essaie :

int trv = consomme(r1, r2, r3);

switch (trv)

Malheureusement, comme cet exemple le montre, cette technique nécessite de tester les codes de re-tour de chaque fonction appelée. 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’erreurs possibles, car il peut en découler un grand nombre de tests et beaucoup de cas particuliers à gérer dans les fonctions appelantes.

Chapitre 8. Les exceptions en C++

Certains programmes résolvent le problème de l’imbrication des tests d’une manière 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ète-ment si tout se passe correctecomplète-ment. 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é ef-fectué. Ainsi, ce code n’est écrit qu’une seule fois, et le traitement des erreurs est situé en dehors du traitement normal.

Exemple 8-2. Gestion des erreurs par sauts

int fait_boulot(const char *rsrc1, const char *rsrc2, const char *rsrc3)

{

int res;

// Alloue la première ressource : res = -1;

int r1 = alloue_ressource(rsrc1);

if (r1 == 0) goto err_alloc_r1;

// Alloue la deuxième ressource : res = -2;

int r2 = alloue_ressource(rsrc2);

if (r2 == 0) goto err_alloc_r2;

// Alloue la troisième ressource : res = -3;

int r3 = alloue_ressource(rsrc3);

if (r3 == 0) goto err_alloc_r3;

// Effectue le boulot :

int trv = consomme(r1, r2, r3);

switch (trv)

Chapitre 8. Les exceptions en C++

err_alloc_r1:

fin_ok:

return res;

}

La solution précédente est tout à fait valable (en fait, c’est même la solution la plus simple), et elle est très utilisée dans les programmes C bien réalisés. Cependant, elle n’est toujours pas parfaite. En effet, il faut toujours conserver l’information de l’erreur dans le code d’erreur si celle-ci doit être remontée aux fonctions appelantes.

Or, des problèmes fondamentaux sont attachés à la notion de code d’erreur. Premièrement, si l’on veut définir des catégories d’erreurs (par exemple pour les erreurs d’entrée/sortie, les erreurs de droits d’accès, et les erreurs de manque de mémoire), il est nécessaire d’utiliser des plages de valeurs dis-tinctes pour chaque catégorie dans le type utilisé pour les codes d’erreur. Cela nécessite une autorité centrale de définition des codes d’erreurs, faute de quoi des conflits de valeurs pour les code, et donc des interprétations erronnées des erreurs, se produiront. Et cela, croyez moi, ce n’est pas facile du tout à gérer (ce n’est d’ailleurs plus un problème de programmation). Deuxièmement, il n’est a priori pas possible de déterminer tous les codes de retour possibles d’une fonction sans consulter sa documen-tation ou son code source. Ce problème impose de définir des traitements génériques, alors qu’ils ne sont a posteriori pas nécessaires.

Comme nous allons le voir, la solution qui met en œuvre les exceptions est une alternative intéres-sante. En effet, la fonction qui détecte une erreur peut se contenter de lancer une exception en lui fournissant le contexte de l’erreur. Cette exception interrompt l’exécution de la fonction, et un ges-tionnaire d’exception approprié est recherché. La recherche du gesges-tionnaire suit le même chemin que celui utilisé lors de la remontée classique des erreurs : à savoir la liste des appelants. Le premier bloc d’instructions qui contient un gestionnaire d’exception capable de traiter cette exception prend donc le contrôle, et effectue le traitement de l’erreur. Si le traitement est complet, le programme reprend son exécution normale. Sinon, le gestionnaire d’exception peut relancer l’exception (auquel cas le gestionnaire d’exception suivant pour ce type d’exception est recherché) ou terminer le programme.

Les gestionnaires d’exceptions capables de traiter une exception sont identifiés par les mécanismes de typage du langage. Ainsi, plusieurs types d’exceptions, a priori définis de manière indépendants, peuvent être utilisés. De plus, le polymorphisme des exceptions permet de les structurer facilement afin de prendre en charge des catégories d’erreurs. Enfin, toutes les informations relatives à l’erreur, et notamment son contexte, sont transmises aux gestionnaires d’exceptions automatiquement par le mécanisme des exceptions du langage. Ainsi, il n’y a plus de risque de conflit lors de la définition de codes d’erreurs, ni de données globales utilisées pour stocker les informations descriptives de l’erreur.

Pour finir, chaque fonction peut spécifier les exceptions qu’elle peut lancer, ce qui permet à l’appelant de savoir à quoi s’attendre.

Les exceptions permettent donc de simplifier le code et de le rendre plus fiable. Par ailleurs, la logique d’erreur est complètement prise en charge par le langage.

Note :L’utilisation des exceptions n’est pour autant pas forcément la meilleure des choses dans un programme. En effet, si un programme utilise du code qui gère les exceptions et du code qui ne gère pas les exceptions (par exemple dans deux bibliothèques tierces), il aura la lourde tâche soit d’encapsuler le code qui ne gère pas les exceptions pour pouvoir l’utiliser correctement, soit de prendre en charge les deux types de traitements d’erreurs directement. La première solu-tion est très lourde et coûteuse, et est difficilement justifiable par des considérasolu-tions de facilité de traitement des erreurs. La deuxième solution est encore pire, le code devenant absolument horrible.

Dans ce genre de situations, mieux vaut s’adapter et utiliser le mécanisme majoritaire. Très sou-vent hélas, il faut abandonner les exceptions et utiliser les notions de codes d’erreur.

Chapitre 8. Les exceptions en C++

Nous allons à présent voir comment utiliser les exceptions en C++.