Je me considérais moi-même avant comme quelqu'un de raisonnablement expert en C++ jusqu'à ce que je lise le chapitre "Exception safety issues and techniques" (en français: problèmes et techniques liées aux exceptions) du livre de Herb Sutter Exceptional C++.
Dans ma tête, connaître les exceptions se limitait à comprendre comment fonctionne un try ... catch. Mais en lisant ce chapitre, où M. Sutter explique au lecteur pâlissant que dans une simple fonction qui concatène des chaînes de caractères, il peut voler plus d'exceptions que dans un livre de grammaire allemande, je me suis rendu compte que ces bestioles méritaient beaucoup plus de respect de ma part, et qu'on ne pouvait raisonnablement s'affirmer expert avant d'avoir au moins intégré tout cela.
Très vite lorsqu'on s'intéresse aux exceptions, deux problèmes majeurs émergent:
- Le programmeur qui doit signaler une erreur doit choisir son mécanisme consciencieusement. Valeur de retour ou exception? Si exception, quelle type d'objet lancer? Faut-il créer sa hiérarchie de classes d'exceptions ou faut-il au contraire utiliser au maximum les classes fournies par la bibliothèque standard (std::exception & co.)?
- Le programmeur qui doit utiliser une fonction ou une classe susceptible de lancer une exception doit écrire son code en conséquence, de manière à ce qu'une exception n'endommage pas l'état du programme de manière incontrôlée.
Par contre écrire du code qui doit jouer le jeu en présence d'exceptions, je le fais tous les jours au bureau. L'idée est que, tout comme l'implémentation d'une fonction que j'utilise m'intéresse moins que le contrat qu'elle prétend honorer, je préfère ne pas savoir si telle ou telle fonction ou méthode lance des exceptions ou pas. Par contre, ce qui m'intéresse, c'est de savoir quel niveau de sécurité est garanti par mon code et par celui des autres face à une exception. Pour cela, il faut comprendre les 3 niveaux de garantie de sécurité, parfois appelées "garanties Abrahams", du nom de celui qui les a formalisé.
Les niveaux de garantie de sécurité face aux exceptions
Prenons un objet o de la classe C, et la méthode C::f() associée. La méthode f() peut se comporter de quatre façons différentes en matière d'exceptions:- Si dans le code de la méthode f() une exception est lancée, l'état du programme est corrompu. Il est alors quasiment impossible de réutiliser l'objet o par la suite, et même sa destruction peut poser problème. La méthode f() ne donne aucune garantie de sécurité.
- Si une exception est lancée de f(), mais qu'une fois l'exécution de f() est finie, l'objet o est toujours dans un état valide (je peux par exemple réinitialiser l'objet ou lui affecter une nouvelle valeur sans risque de plantage), alors la méthode f() offre la garantie de base: une exception laisse l'objet dans un état valide, même si cet état n'est pas forcément prévisible.
- Si en plus d'offrir la garantie de base, f() se comporte en cas d'échec comme si elle n'avait jamais été appelée, elle offre la garantie forte: l'état de l'objet reste inchangé si une opération ne peut être complétée.
- Si f() ne peut jamais lancer d'exception, on parle de garantie no-throw.
A noter que parfois, monter d'un cran en matière de garantie de sécurité peut avoir des conséquences néfastes pour la performance, parce qu'il faut alors faire plus de travail et/ou utiliser plus de mémoire. Il peut donc y avoir des raisons parfaitement valables pour se limiter par exemple à la garantie de base au lieu de fournir les garanties forte ou no-throw.
Exemples
Lorsque je joue à la Playstation (PS2), je vois régulièrement des écrans de sauvegarde qui me demandent poliment de ne pas éteindre la console le temps de l'opération. Typiquement, voilà une opération qui n'offre aucune garantie de sécurité: si une "exception" se produit pendant son exécution (le chat fait tomber un pot de fleurs sur l'interrupteur de la multiprise), le fichier qu'aura tenté d'écrire le jeu sera probablement complètement inutilisable. Je ne pourrai plus jamais le relire, et pire encore, il se pourrait même que la carte mémoire entière soit devenue inutilisable. C'est mal.Il vous est sans doute déjà arrivé de vouloir copier un gros répertoire d'un disque à un autre, et voir apparaître en cours de route une erreur vous disant que pour une raison ou pour une autre, une erreur s'est produite lors de la copie. Tous les OS que je connais offrent pour une copie récursive d'un répertoire la garantie de base: la copie ne sera peut-être pas complète, je ne saurais même pas forcément quels fichiers auront été copiés et lesquels doivent encore l'être à moins d'aller voir par moi-même et compter à la main, mais au moins je sais que ni mon disque ni le système de fichiers n'auront été endommagés. Je peux effacer le répertoire cible pour recommencer, ou copier le reste des fichiers à la main, etc. Pas trop mal.
Lorsque vous retirez de l'argent à un distributeur automatique de billets, il peut se passer plein d'"exceptions": vous pouvez vous tromper de code PIN, votre banque peut vous refuser le retrait, il peut y avoir une coupure de courant ou de communication avec la banque, etc. Dans tous les cas, si vous n'avez pas l'argent en main à la fin de l'opération, vous ne voulez surtout pas que la somme soit débitée de votre compte! Et, heureusement, c'est aussi bien le cas: soit une transaction réussit complètement et l'argent est passé de votre compte à votre main, soit elle échoue complètement et tout l'argent est resté sur votre compte. C'est ça, la garantie forte. C'est bien.
Les opérations offrant la garantie no-throw sont plus rares, mais en tant que programmeur, vous en avez quelques-unes à votre disposition. Affecter une valeur à un type de base, genre x = 10 dans le cas ou x est un int, en est une. Autre exemple: un destructeur ne devrait jamais lancer d'exception, et pourtant les destructeurs font souvent des choses complexes, comme par exemple désallouer de la mémoire. Qu'à cela ne tienne, désallouer de la mémoire revient souvent à changer les valeurs de quelques pointeurs, ce qui ne lance pas d'exception. Parfait!
Conclusion
Lorsque vous écrivez une fonction, ayez en tête le niveau de garantie de sécurité qu'elle offre face aux exceptions. Ayez aussi en tête celles des fonctions que vous utilisez, et notamment celles des fonctions de la STL. Rappelez-vous que lorsque vous utilisez des templates, vous pouvez vous retrouver avec un type dont le constructeur de copie, l'opérateur d'affectation ou encore l'opérateur d'égalité peuvent lancer des exceptions.Si l'anglais ne vous pose pas de problème, lisez Exceptional C++, qui est très bien écrit et contient quantité de conseils formidables (contrairement à ce que laisse penser le titre, le livre ne parle pas que d'exceptions en C++). Ou à défaut, stay tuned comme ils disent là-bas, car il y aura sans doute d'autres articles sur le sujet ici-même.