Dans le contexte des langages de programmation fonctionnels et impératifs, un système de gestion d'exceptions ou SGE permet de gérer les conditions exceptionnelles pendant l'exécution du programme. Lorsqu'une exception se produit, l'exécution normale du programme est interrompue et l'exception est traitée.
Les erreurs/exceptions les plus courantes sont probablement l'accès non autorisé à une zone mémoire (erreur de manipulation de pointeur) et la division par zéro (on ne prévoit pas le cas où le diviseur est nul).
Intérêt
Tout programme en exécution peut être sujet à des erreurs pour lesquelles des stratégies de détection et de réparation sont possibles. Ces erreurs ne sont pas des bugs mais des conditions particulières (ou conditions exceptionnelles, ou exceptions) dans le déroulement normal d'une partie d'un programme.
Par exemple, l'absence d'un fichier utile n'est pas un bogue du programme ; par contre, ne pas gérer son absence en provoquerait un.
Le traitement des situations exceptionnelles fait apparaître deux besoins : une syntaxe spéciale, pour distinguer l'exécution normale du traitement des erreurs, un flot de contrôle « non local », pour traiter et réparer les situations exceptionnelles.
Dans les langages de programmation sans SGE, on n'a pas d'outil pour séparer l'exécution normale et l'exécution exceptionnelle du programme. Un algorithme, dont l'exécution normale s'exprime de façon simple et élégante, peut devenir illisible (et donc difficile à maintenir) une fois « enrobé » par une logique de traitement des situations exceptionnelles ; disposer dans un langage de programmation d'une syntaxe pour différencier l'exécution normale de l'exécution dans un contexte exceptionnel peut être utile.
Le traitement d'une situation exceptionnelle peut nécessiter de revenir « dans le passé » de l'exécution du programme, c'est-à-dire remonter brutalement la chaîne d'appels pour annuler une opération fautive, ou encore modifier les valeurs de certaines variables, puis reprendre l'exécution du programme un peu avant le site de l'erreur. D'où le besoin d'associer, à la syntaxe spéciale, des opérateurs spéciaux pour effectuer des sauts et des modifications de variables à des points arbitraires de la chaîne d'appels.
Principes généraux
Types d'erreur Les exceptions suivantes peuvent survenir dans pratiquement tous les programmes : arithmétique (débordement, division par zéro…) ; collections (débordement d'indices) ; allocation mémoire (mémoire insuffisante) ; signaux système (interruption du programme par le système d'exploitation).
Dans certains langages à objets, le type d'une exception doit impérativement être une classe. Une hiérarchie prédéfinie et extensible de types d'exceptions, correspondant au type des erreurs qu'elles représentent, est fournie. D'autres langages, comme C++, autorisent aussi les types primitifs.
Gestionnaire d'exceptions
Un gestionnaire d'exception établit un ensemble de routines de traitement d'erreurs, définies par le programmeur, sur un bloc (dans une fonction ou une méthode du programme) ; ces routines sont activées pendant toute la durée d'exécution du bloc protégé.
La notion d'exécution du bloc protégé inclut toute la chaîne d'appels (de fonctions, procédures ou méthodes) à partir de ce bloc : on dit que les gestionnaires d'exception sont actifs dans la portée dynamique du bloc.
Le signalement d'une exception peut être automatique, s'il correspond à une exception définie dans le langage de programmation ou une bibliothèque fournie, ou bien déclenché par le programmeur par l'utilisation d'une primitive de signalement. Il est généralement possible de construire de nouveaux types d'exceptions et de programmer leur signalement.
Le gestionnaire d'exception peut être complété par un ensemble de restarts, qui sont des routines permettant la modification des environnements lexicaux entre le site de signalement et le point d'établissement des gestionnaires d'exception. Un restart permet à un gestionnaire d'exception de choisir de réparer et redémarrer un calcul plutôt que de l'abandonner intégralement. Un restart est également actif dans la portée dynamique du bloc sur lequel il est défini.
Signalement d'une erreur Lorsqu'une condition d'erreur est détectée (par une primitive de signalement, une trappe processeur, le système d'exploitation ou encore l'environnement d'exécution du programme), on dit qu'elle est signalée : un bloc de traitement d'erreur (un handler) est recherché dans la liste des gestionnaires actifs. L'exécution du programme est déférée au bloc de traitement, qui effectue des actions correctrices et décide si le calcul où l'erreur a été signalée est terminé ou bien repris (si c'est possible, c'est-à-dire en présence de restarts).
Il se peut qu'aucun gestionnaire n'ait été prévu par le programmeur, auquel cas un gestionnaire par défaut, dont le comportement est prédéfini, est sélectionné.
Réparation d'une erreur, reprise
Dans un bloc de traitement, le programmeur a deux options : arrêt brutal du calcul fautif et choix d'un autre calcul : la pile d'appel du bloc protégé est alors détruite ; réparations en différents points de la chaîne d'appel ayant conduit à l'erreur, et reprise du calcul à un lieu préalablement défini.
Opérateurs Signalement d'une condition d'erreur (ne détruit pas la pile) : signal, error, cerror… L'installation d'un bloc de traitement d'erreur est réalisé avec des primitives du type : try/catch/finally, handler-bind… L'installation d'un restart (un bloc de réparation de contexte lexical) est permise par : restart-case, restart-bind… La destruction de la pile d'appel entre l'expression signalant une exception et le bloc de traitement, ou un point de reprise est effectuée par : throw, raise En Python, par exemple, le signalement implique la destruction de la pile d'appels jusqu'au premier bloc de traitement disponible. Dans ce cas, throw (ou raise) est la seule primitive de signalement, et la réparation et la reprise sont impossibles.
Dans les langages de programmation
BASIC
Le langage Basic comporte un système de traitement d'erreurs de type "piège à erreurs", mais qui n'en est pas moins une gestion d'exceptions.
function toto(x as double) as double DIM f as double f = 1.0 REM on affecte la valeur 1, jusqu'ici tout va bien ON LOCAL ERROR GOTO MonException <span style="color: purple;">REM [try] on débute un bloc protégé</span> f = 1.0 / 0.0 REM provoque une division par zéro, ou alors f vaut +infini f = sin(f) REM sinus de +infini est indéfini sinon toto = f REM Retourne le résultat</span> EXIT FUNCTION REM Indispensable, sinon on entre dans la zone de traitement d'exception REM [except] MonException: <span style="color: purple;">REM on gère les exceptions
REM Autre cas: Correction puis poursuite</span> f = 1.0 RESUME NEXT <span style="color: purple;">REM Passe à la ligne suivante (f = sin(f)) toto = f end function;
C++ Une erreur est déclenchée avec le mot-clé throw. Les erreurs peuvent être interceptées avec un bloc try-catch. Un simple throw; dans un bloc catch permet de relancer l’erreur en cours.
Le type utilisé dans catch interceptera tous les types d’erreur compatibles ; si le type réel de l’erreur peut être converti dans le type de la variable par polymorphisme. Dans l’exemple suivant std::exception est une base publique de std::runtime_error qui peut donc l’intercepter.try { throw std::runtime_error("Erreur"); } catch (std::exception & erreur) { std::cerr << erreur.what() << '\n'; }
Déroulement de pile et destruction Le déroulement des exceptions respecte le principe RAII du C++ : les variables locales initialisées avant le déclenchement de l’erreur sont détruites dans l’ordre inverse et le mot clé finally n’existe pas. Certains types de la bibliothèque standard permettent d’exploiter ce mécanisme. Le type lock_guard permet de libérer un verrou dès la sortie du contexte ou à la moindre erreur. Cependant si une erreur est déclenchée durant la destruction d’un objet alors que le programme est déjà en train de défaire la pile à cause d’une erreur précédente, et que cette nouvelle erreur n’est pas interceptée, alors la fonction std::terminate est déclenchée pour terminer immédiatement le programme sans précautions.
Si une erreur a lieu durant l’initialisation d’un membre d’une classe ou durant la fonction spéciale du constructeur alors la fonction spéciale destructeur ne seront pas déclenchées car l’objet n’est pas totalement initialisé. Les membres déjà initialisés seront détruits normalement. C’est pourquoi il faut éviter au maximum l’apparition d’erreur durant la création d’un objet.class Incomplet { std::unique_ptr<int> c;
Incomplet(): c(std::make_unique<int>(5)) { throw std::runtime_error("");
~Incomplet() { // fonction non exécutée. c.reset(); // Le membre c est néanmoins détruit par le RAII et la mémoire libérée.
Coût Les implémentations modernes du système d’exceptions permettent un coût pratiquement nul pour un déroulement normal du programme mais le traitement d’une erreur est bien plus grand qu’un simple retour de fonction. Les exceptions doivent donc rester exceptionnelles ! Le standard C++ définit également le type std::expected pour permettre à une fonction de retourner au choix une valeur ou une erreur. Ce mécanisme est identique au Result de Rust.
Delphi / Lazarus
Exemple en Delphi ou en Lazarus :
procedure TForm1.Button1Click(Sender: TObject); var f: Real; // soit f un nombre réel</span> begin f := 1; <span style="color: purple;">// on affecte la valeur 1, jusqu'ici tout va bien
try // on débute un bloc protégé</span> f := 1/0; <span style="color: purple;">// provoque une division par zéro, ou alors f vaut +infini f := sin(f); // sinus de +infini est indéfini sinon</span>
except <span style="color: purple;">// on gère les exceptions on e: Exception do // on appelle e l'exception qui vient d'arriver</span> Application.MessageBox( PChar('Message : '+e.Message), 'Exception', 0); end; <span style="color: purple;">// fin du bloc protégé
Application.MessageBox( PChar('Valeur de f : '+FloatToStr(f)), 'Resultat', 0); end;
Java
Java offre un SGE terminal, donc sans réparation ni reprise.
Exemple de traitement d'une division par zéro : public class FooBar { FooBar () { }
int foo (String b) { int resultat; try { resultat = bar (b);
catch (Exception e) { System.out.println ("erreur pendant l'exécution de bar : " + e.toString ()); resultat = 666;
return resultat;
int bar (String s) { System.out.println ("tiens, un " + s); System.out.println ("faisons quelque chose de mal..."); int a = 42 / 0; // <- quelque chose de mal a = a + 7; return a;
Particularités Dans CLU [Lyskov-Snyder 79] et Java, une distinction est faite entre :
les exceptions déclarées dans la signature d'une méthode (CheckedException en Java) ; par exemple
void foo () throws ThisExceptionType { ... },
les exceptions à l'exécution (RuntimeException en Java), qui correspondent à des évènements impossibles à localiser lexicalement à la compilation (les exceptions asynchrones), ou pouvant survenir à tout moment dans l'exécution du programme, comme les problèmes d'allocation mémoire.
Les checked exceptions essayent de résoudre un problème de contrat. L'interface d'un module (d'une bibliothèque de classes) représente un contrat entre l'auteur du module et son utilisateur : l'argument est qu'un tel contrat ne devrait pas passer sous silence les exceptions susceptibles d'être propagées hors des frontières du module.
En spécifiant les exceptions dans les signatures des méthodes, on introduit toutefois un problème. En effet, les méthodes clientes doivent choisir dans l'alternative :
installer un GE pour les exceptions du module ;
ou bien déclarer à leur tour ces exceptions.