Programmation C-C++/Les exceptions

Cours de C/C++
^
Les exceptions
Lancement et récupération d'une exception
Remontée des exceptions
Liste des exceptions autorisées pour une fonction
Hiérarchie des exceptions
Exceptions dans les constructeurs

Livre original de C. Casteyde

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