| ||
auteur : Aurélien Regat-Barrel | ||
Les exceptions sont un nouveau moyen de gérer les erreurs dans les programmes. La grande différence vis à vis du classique code d'erreur renvoyé par une fonction est qu'une exception se propage depuis l'appelé vers l'appelant jusqu'à ce qu'elle rencontre un bloc de code qui s'occupe de la traiter. Au contraire d'un code d'erreur qui peut être ignoré (ce qui est malheureusement souvent le cas) une exception doit être traitée. Si elle ne l'est pas dans la fonction qui en est à l'origine, elle doit l'être dans l'une des fonctions appelantes. Le compilateur s'occupe tout seul de faire en sorte que l'exception remonte le long de la pile des appels jusqu'à l'endroit où un bloc a été prévu pour la traiter. Cela permet donc de facilement faire "remonter" les erreurs des fonctions appelées vers les fonctions appelantes. L'apparition d'une exception interrompt l'exécution normale du programme et provoque sa reprise dans le gestionnaire d'exception le plus proche (qui peut se trouver beaucoup plus en amont dans une fonction appelante). Le programmeur n'a donc plus à se soucier de tester la réussite ou non des fonctions qu'il appelle au moyen d'un grand nombre de tests comme dans l'exemple suivant :
Les exceptions permettent de grandement simplifier le code précédent tout en améliorant la qualité du renseignement sur l'origine de l'erreur :
|
| ||||||
auteur : Aurélien Regat-Barrel | ||||||
Les exceptions sont déclenchées grâce à l'utilisation du mot-clé throw :
Les exceptions peuvent être de n'importe quel type (type de base du langage ou classe quelconque). Mais il est conseillé d'utiliser une classe dérivant de la classe standard std::exception définie dans le fichier d'en-tête standard <exception>. Cette classe dispose d'une fonction membre what() qui renvoie une description de l'exception.
Les exceptions doivent être de préférence déclenchées par valeur, et attrapée par référence. Déclencher une exception par pointeur pose en effet un problème :
Le code précédent fonctionne mais une question subsiste : comment est libéré le pointeur alloué dans le bloc try ? La réponse est qu'il ne l'est pas, et il se produit donc une fuite de mémoire. Le problème de déclencher une exception par pointeur est donc de savoir à qui incombe la responsabilité de libérer ce dernier. Voilà pourquoi on préfère lever des exceptions par valeur, et les attraper par référence (de préférence constante) afin de permettre le polymorphisme et d'améliorer les performances en évitant une recopie de l'objet.
Un cas particulier est celui des chaînes de caractères littérales :
Dans l'exemple précédent, bien qu'on utilise un pointeur, il n'y a pas de fuite de mémoire tout simplement parce qu'il n'y a pas d'allocation dynamique. Ce serait même une grosse erreur de libérer ce pointeur avec delete [] car ce dernier pointe vers une zone de mémoire spéciale (généralement en lecture seule) qui contient l'ensemble de chaînes de caractères littérales utilisées dans le programme. Donc, à priori, utiliser des chaînes littérales est une bonne idée, mais cela est néanmoins déconseillé car leur utilité est vite limitée du fait de l'impossibilité de formater les chaînes pour donner la valeur d'une variable par exemple. Il y a fort à craindre que tôt ou tard un programmeur voudra effectuer cette opération et provoquera alors inconsciemment une fuite de mémoire (car dans le bloc catch il n'y a aucun moyen de distinguer une chaîne littérale d'une chaîne allouée avec new []). Si vous persistez à vouloir utiliser de simple chaînes de caractères au lieu d'une classe dérivant de std::exception, utilisez au moins le type chaîne de caractères du C++ : std::string.
|
| ||||
auteur : Aurélien Regat-Barrel | ||||
Le code susceptible de déclencher des exceptions doit être placé dans un bloc try...catch (essaye...attrape) de cette manière :
On peut mettre autant de blocs catch qu'il y a d'exceptions à rattraper.
Les exceptions levées dans le bloc try vont être filtrées par les différents blocs catch suivant leur ordre d'apparition. Ce filtrage est effectué en fonction du type de l'exception, et est naturellement sensible au polymorphisme. C'est à dire que le premier bloc catch rencontré capable de traiter l'exception levée est celui qui est utilisé. Les autres sont ignorés, même si certains seraient mieux adaptés. En l'occurrence, dans l'exemple précédent, l'exception out_of_range est levée et ce serait tout naturellement le dernier bloc catch qui devrait la traiter. Mais std::out_of_range dérive de std::exception qui est la classe de base pour les exceptions standards, et donc c'est le premier bloc catch qui est appelé. Il existe aussi un moyen d'attraper toutes les exceptions, en utilisant une ellipse (...) comme type de l'exception. Mais alors il n'y a aucun moyen de connaître l'origine et le type de l'exception (sauf à la relancer et la traiter dans un nouveau bloc try...catch). L'utilisation de cette forme générique doit être restreinte car elle ne permet de savoir si l'exception capturée peut être traitée et ignorée ou si elle nécessite de terminer le programme (corruption de la mémoire, etc...). On l'utilise en général comme dernier recours.
Il est donc important de faire apparaître les blocs catch des classes dérivées en premier. Notez que les exceptions sont récupérées par référence, comme cela est expliqué dans la question Pourquoi faut-il capturer les exceptions par référence ?. Ces références ne sont cependant pas des références sur l'objet initial qui est à l'origine de l'exception, mais sur une copie de celui-ci, car l'objet initial risque d'être détruit si l'on quitte la fonction qui a levé l'exception. |
| ||
auteur : Aurélien Regat-Barrel | ||
Comme discuté dans la question Comment lever une exception ?, il est fortement recommandé de lever des exceptions par valeur. En revanche, il vaut mieux les capturer par référence et non pas par valeur. Tout d'abord cela permet d'éviter une recopie, mais aussi et surtout cela permet de conserver le polymorphisme. L'exemple suivant illustre les problèmes posés par un traitement des exceptions par valeur :
Cet exemple affiche le message Unknown exception. Si l'on remplace le traitement par valeur par un traitement par référence :
Alors on obtient le message attendu exception de test. Ceci est dû au fait que le polymorphisme nécessite d'utiliser un pointeur ou une référence, autrement dans notre cas l'objet de type std::logic_error est "tronqué" en un objet de type std::exception. La fonction membre what appelée n'est donc pas celle de std::logic_error mais celle de std::exception, qui n'est pas d'une grande utilité. Donc à moins de rechercher volontairement ce comportement, il est recommandé de traiter les exceptions par référence, de préférence constantes afin de permettre au compilateur d'effectuer des optimisations. |
| ||
auteur : LFE | ||
Malheureusement, non, ce mécanisme n'est pas possible. Un catch ne pouvant capturer qu'un seul type d'exceptions, il faut définir autant de
blocs try/catch qu'il y a d'exceptions possibles.
L'utilisation de
permet de capturer toutes les exceptions pouvant survenir, mais il est, dans ce cas, impossible de faire la distinction.
|
| ||
auteur : Aurélien Regat-Barrel | ||
Le mot-clé throw permet de lever une nouvelle exception, mais aussi de relancer celle qui est en cours de traitement.
|
| ||
auteur : Aurélien Regat-Barrel | ||
Lorsqu'une exception est déclenchée, le compilateur recherche un bloc catch capable de la traiter. S'il n'en trouve pas, il remonte la pile d'exécution (déroulage de la pile) afin d'en trouver un plus en amont dans la hiérarchie des appels. Dépiler un appel revient à quitter une fonction. A cette occasion ses objets locaux sont détruits et les destructeurs appelés, ce qui permet de quitter proprement la fonction en libérant toutes les ressources acquises si les destructeurs on été bien écrits. L'objet qui a servi à lever l'exception est lui même détruit car il est local à la fonction. C'est pourquoi l'objet qui est transmis aux blocs catch est toujours une copie de l'objet initial qui a déclenché l'exception. Si la pile des appels est vidée (donc que l'on est arrivé à main) et qu'aucun bloc catch satisfaisant n'a été trouvé, la fonction standard terminate est appelée ce qui provoque par défaut un arrêt pur et simple du programme. Ce comportement peut être modifié au moyen de la fonction set_terminate définie dans l'en-tête standard <exception>. Cette fonction installe un nouveau gestionnaire et renvoie l'adresse du précédent. Elle s'utilise de cette manière :
Le code précédent provoque le résultat suivant avec le compilateur Visual C++ 7.1 :
Exception inattendue.
This application has requested the Runtime to terminate it in an unusual way.
Please contact the application's support team for more information.
|
| ||
auteur : Aurélien Regat-Barrel | ||
N'importe quel type de base ou classe C++ peut être utilisé comme type d'exception. Mais il est préférable de créer son type qui hérite de la classe de base standard pour les exceptions : std::exception définie dans l'en-tête <exception>. Cette classe possède une fonction membre virtuelle what qu'il convient de redéfinir :
Cet exemple produit le résultat suivant :
Erreur ligne 29 : exception test
|
| |||
auteurs : Aurélien Regat-Barrel, Luc Hermitte | |||
Tout à fait. C'est même une des seules manières d'indiquer que l'initialisation de l'objet a échoué. Il faut cependant être prudent car une exception levée dans un constructeur peut être à l'origine de fuites de mémoires ou d'autres problèmes de non libération de ressources. C'est le cas dans l'exemple suivant :
L'idée du code ci-dessus est d'initialiser les pointeurs tableau1 et tableau2 à zéro ainsi si l'une des allocations échoue on peut tout de même appeler en toute sérénité delete [] dans le destructeur et ainsi éviter les fuites de mémoire (souvenez vous, appeler delete sur un pointeur nul ne fait rien, voir Que se passe-t-il si je fais un delete sur un pointeur qui vaut NULL ?). Le problème est que si une exception est levée lors de la construction d'un objet, c'est donc que celle-ci a échoué, et donc que l'objet n'est pas créé. Comme il n'est pas créé, il n'a pas à être détruit, et donc son destructeur ne sera pas appelé. Autrement dit, si une exception est levée dans le constructeur d'un objet, son destructeur ne sera pas appelé. Il faut donc toujours s'assurer que le code contenu dans le constructeur est exception safe, c'est à dire qu'il résiste aux exceptions en ne provoquant pas de pertes de ressources. Dans notre exemple précédent cela signifie qu'il faut gérer l'exception bad_alloc de cette manière :
Le nouveau code produit toujours une exception bad_alloc en cas d'échec d'allocation, mais cette fois-ci il n'y a plus de fuite de mémoire. Ce qui a pu être alloué est libéré, et l'exception bad_alloc capturée est relancée via l'instruction throw (à ce sujet lire Comment relancer une exception que l'on a capturé ?). Il est à noter que si en cas d'exception dans le constructeur le destructeur n'est pas appelé, tous les membres construits jusqu'au point de l'exception sont quant à eux bien détruits. |
| ||
auteur : Aurélien Regat-Barrel | ||
Il est possible de lever une exception dans un destructeur, mais c'est extrêmement déconseillé et considéré comme une très mauvaise pratique. La raison en est simple : si la destruction d'un objet échoue, que faut-il faire ? Mais aussi un autre problème plus grave peut apparaître. Lorsqu'une exception est levée, la pile des appels est remontée (on parle de stack unwinding ou déroulage de la pile) et à cette occasion les objets locaux de la fonction que l'on s'apprête à quitter sont détruits. Donc leur destructeur respectif est appelé. Si l'un d'entre eux vient à lever une exception, la situation devient alors très complexe : laquelle des deux exceptions faut-il gérer ? N'oubliez pas qu'à ce moment nous ne sommes toujours pas dans un bloc catch, mais en train de nous y rendre en quittant les fonctions appelées qui nous en sépare.
Or, en quittant l'une d'entre elles, on détruit un objet qui lance une nouvelle exception, et nous nous retrouvons alors avec deux exceptions à traiter en même temps. La situation étant insoluble, la norme définit que la fonction standard terminate est appelée dans un tel cas, ce qui provoque la fin brutale du programme. Pour cette très bonne raison, il est important que les destructeurs ne lèvent jamais d'exceptions. On peut s'en assurer en appelant uniquement des fonctions n'échouant jamais (voir Comment indiquer qu'une fonction ne lève jamais d'exception ?). | ||
lien : Question de la C++ FAQ Lite ayant inspiré cette réponse |
| ||
auteur : Aurélien Regat-Barrel | ||
Pour indiquer qu'une fonction ne lève jamais d'exceptions (ce qui est important si on veut l'appeler depuis un destructeur par exemple (voir Peut-on lever des exceptions dans les destructeurs ?), on rajoute l'instruction throw () à la suite du prototype de la fonction de cette façon :
Attention à bien faire en sorte qu'aucune exception ne soit effectivement levée, car la norme dit que si tel était le cas la fonction standard unexpected serait appelée, ce qui se traduirait par un arrêt brutal du programme.
|
| ||
auteur : Aurélien Regat-Barrel | ||
Dans certains langages (tels que Java ou C#), il est possible de créer un bloc finally à la suite d'un bloc try...catch afin de s'assurer
qu'une opération (de libération de ressource par exemple) soit bien effectuée. Par exemple, en C++ on ne peut pas écrire ceci :
l'équivalent de l'écriture ci-dessus serait :
Mais il ne s'agit là que d'une traduction en C++ d'une approche issue d'un autre langage. Or, quand on programme dans un langage, il
convient de le faire selon les concepts propres à ce langage, et non avec ceux d'un autre. L'approche C++ à ce problème consiste à
encapsuler cette gestion au sein d'un objet qui s'assurera dans son destructeur de la bonne libération de la ressource qu'il gère.
Ainsi, on est assuré de ne pas avoir de fuite même en cas d'exception, tout en ayant une écriture plus légère car le bloc try...catch devient
inutile dans ce cas. Ce principe s'appelle le RAII, et est développé dans la question Comment gérer proprement des allocations / désallocations de ressources : le RAII. |
Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2008 Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.