Votre question fait une affirmation, que «l'écriture de code d'exception est très difficile». Je vais d'abord répondre à vos questions, puis répondre à la question cachée derrière elles.
Répondre à des questions
Écrivez-vous vraiment du code d'exception?
Bien sur que oui.
C'est la raison pour laquelle Java a perdu beaucoup de son attrait pour moi en tant que programmeur C ++ (manque de sémantique RAII), mais je m'égare: c'est une question C ++.
Il est en effet nécessaire lorsque vous devez travailler avec du code STL ou Boost. Par exemple, les threads C ++ ( boost::thread
ou std::thread
) lèveront une exception pour quitter correctement.
Êtes-vous sûr que votre dernier code "prêt pour la production" est protégé contre les exceptions?
Pouvez-vous même être sûr que c'est le cas?
Écrire du code sans exception est comme écrire du code sans bogue.
Vous ne pouvez pas être sûr à 100% que votre code est protégé contre les exceptions. Mais ensuite, vous vous efforcez de le faire, en utilisant des modèles bien connus et en évitant les anti-modèles bien connus.
Connaissez-vous et / ou utilisez-vous réellement des alternatives qui fonctionnent?
Il n'y a pas d' alternative viable en C ++ (c'est-à-dire que vous devrez revenir en C et éviter les bibliothèques C ++, ainsi que les surprises externes comme Windows SEH).
Écriture d'un code d'exception sécurisé
Pour écrire du code d'exception, vous devez d' abord connaître le niveau de sécurité d'exception de chaque instruction que vous écrivez.
Par exemple, un new
peut lever une exception, mais l'attribution d'un intégré (par exemple un int ou un pointeur) n'échouera pas. Un échange n'échouera jamais (n'écrivez jamais un échange de lancer), un std::list::push_back
peut lancer ...
Garantie d'exception
La première chose à comprendre est que vous devez pouvoir évaluer la garantie d'exception offerte par l'ensemble de vos fonctions:
- aucun : votre code ne devrait jamais offrir cela. Ce code va tout fuir et tomber en panne à la toute première exception levée.
- basique : c'est la garantie que vous devez à tout le moins offrir, c'est-à-dire que si une exception est levée, aucune ressource n'est perdue et tous les objets sont toujours entiers
- strong : le traitement réussira ou lèvera une exception, mais s'il lève, les données seront dans le même état que si le traitement n'avait pas du tout démarré (cela donne un pouvoir transactionnel à C ++)
- nothrow / nofail : le traitement réussira.
Exemple de code
Le code suivant semble être C ++ correct, mais en vérité, offre la garantie "none", et donc, il n'est pas correct:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
X * x = new X() ; // 2. basic : can throw with new and X constructor
t.list.push_back(x) ; // 3. strong : can throw
x->doSomethingThatCanThrow() ; // 4. basic : can throw
}
J'écris tout mon code avec ce genre d'analyse à l'esprit.
La garantie la plus basse offerte est basique, mais ensuite, l'ordre de chaque instruction rend la fonction entière "aucune", car si 3. lance, x fuira.
La première chose à faire serait de rendre la fonction "basique", c'est-à-dire de mettre x dans un pointeur intelligent jusqu'à ce qu'il soit détenu en toute sécurité par la liste:
void doSomething(T & t)
{
if(std::numeric_limits<int>::max() > t.integer) // 1. nothrow/nofail
t.integer += 1 ; // 1'. nothrow/nofail
std::auto_ptr<X> x(new X()) ; // 2. basic : can throw with new and X constructor
X * px = x.get() ; // 2'. nothrow/nofail
t.list.push_back(px) ; // 3. strong : can throw
x.release() ; // 3'. nothrow/nofail
px->doSomethingThatCanThrow() ; // 4. basic : can throw
}
Désormais, notre code offre une garantie "basique". Rien ne fuira et tous les objets seront dans un état correct. Mais nous pourrions offrir plus, c'est-à-dire la garantie solide. C'est là que cela peut devenir coûteux, et c'est pourquoi tout le code C ++ n'est pas fort. Essayons:
void doSomething(T & t)
{
// we create "x"
std::auto_ptr<X> x(new X()) ; // 1. basic : can throw with new and X constructor
X * px = x.get() ; // 2. nothrow/nofail
px->doSomethingThatCanThrow() ; // 3. basic : can throw
// we copy the original container to avoid changing it
T t2(t) ; // 4. strong : can throw with T copy-constructor
// we put "x" in the copied container
t2.list.push_back(px) ; // 5. strong : can throw
x.release() ; // 6. nothrow/nofail
if(std::numeric_limits<int>::max() > t2.integer) // 7. nothrow/nofail
t2.integer += 1 ; // 7'. nothrow/nofail
// we swap both containers
t.swap(t2) ; // 8. nothrow/nofail
}
Nous avons réorganisé les opérations, en créant d'abord et en les réglant X
à leur juste valeur. Si une opération échoue, elle t
n'est pas modifiée, donc les opérations 1 à 3 peuvent être considérées comme "fortes": si quelque chose se déclenche, t
n'est pas modifié et X
ne fuira pas car il appartient au pointeur intelligent.
Ensuite, nous créons une copie t2
de t
et travaillons sur cette copie de l'opération 4 à 7. Si quelque chose se lève, t2
est modifié, mais t
reste l'original. Nous offrons toujours la garantie solide.
Ensuite, nous échangeons t
et t2
. Les opérations de swap doivent être nothrow en C ++, alors espérons que le swap pour T
lequel vous avez écrit est nothrow (si ce n'est pas le cas, réécrivez-le pour qu'il ne soit pas throw).
Donc, si nous atteignons la fin de la fonction, tout a réussi (pas besoin d'un type de retour) et t
a sa valeur exceptée. S'il échoue, il t
a toujours sa valeur d'origine.
Maintenant, offrir la garantie forte pourrait être assez coûteux, alors ne vous efforcez pas d'offrir la garantie forte à tout votre code, mais si vous pouvez le faire sans coût (et l'incrustation C ++ et d'autres optimisations pourraient rendre tout le code ci-dessus gratuit) puis faites-le. L'utilisateur de la fonction vous en remerciera.
Conclusion
Il faut une certaine habitude pour écrire du code sans exception. Vous devrez évaluer la garantie offerte par chaque instruction que vous utiliserez, puis vous devrez évaluer la garantie offerte par une liste d'instructions.
Bien sûr, le compilateur C ++ ne sauvegardera pas la garantie (dans mon code, j'offre la garantie en tant que balise doxygen @warning), ce qui est un peu triste, mais cela ne devrait pas vous empêcher d'essayer d'écrire du code sans exception.
Échec normal vs bug
Comment un programmeur peut-il garantir qu'une fonction sans échec réussira toujours? Après tout, la fonction pourrait avoir un bug.
C'est vrai. Les garanties d'exception sont censées être offertes par un code sans bogue. Mais alors, dans n'importe quel langage, appeler une fonction suppose que la fonction est exempte de bogues. Aucun code sain ne se protège contre la possibilité d'un bogue. Écrivez le code du mieux que vous pouvez, puis offrez la garantie en supposant qu'il est exempt de bogues. Et s'il y a un bug, corrigez-le.
Les exceptions concernent les échecs de traitement exceptionnels, pas les bogues de code.
Derniers mots
Maintenant, la question est "Est-ce que ça vaut le coup?".
Bien sûr que oui. Avoir une fonction "nothrow / no-fail" sachant que la fonction n'échouera pas est une grande aubaine. La même chose peut être dite pour une fonction "forte", qui vous permet d'écrire du code avec une sémantique transactionnelle, comme les bases de données, avec des fonctionnalités de validation / restauration, la validation étant l'exécution normale du code, la levée d'exceptions étant la restauration.
Ensuite, le «basique» est la moindre garantie que vous devriez offrir. Le C ++ est un langage très puissant, avec ses étendues, vous permettant d'éviter toute fuite de ressources (quelque chose qu'un garbage collector aurait du mal à offrir pour la base de données, la connexion ou les descripteurs de fichiers).
Donc, pour autant que je le vois, il est la peine.
Edit 2010-01-29: A propos du swap sans lancer
nobar a fait un commentaire qui, je pense, est tout à fait pertinent, car il fait partie de "comment écrivez-vous le code d'exception":
- [moi] Un échange n'échouera jamais (n'écrivez même pas un échange de lancement)
- [nobar] Ceci est une bonne recommandation pour les
swap()
fonctions écrites sur mesure. Il convient toutefois de noter que cela std::swap()
peut échouer en fonction des opérations qu'il utilise en interne
la valeur par défaut std::swap
fera des copies et des affectations qui, pour certains objets, peuvent être lancées. Ainsi, le swap par défaut peut être lancé, utilisé pour vos classes ou même pour les classes STL. En ce qui concerne la norme C ++, l'opération de permutation pour vector
, deque
et list
ne lancera pas, alors qu'elle le pourrait pour map
si le foncteur de comparaison peut lancer sur la construction de copie (voir The C ++ Programming Language, Special Edition, appendix E, E.4.3 .Échanger ).
En regardant l'implémentation Visual C ++ 2008 du swap du vecteur, le swap du vecteur ne lancera pas si les deux vecteurs ont le même allocateur (c'est-à-dire le cas normal), mais fera des copies s'ils ont des allocateurs différents. Et donc, je suppose qu'il pourrait jeter dans ce dernier cas.
Ainsi, le texte original tient toujours: N'écrivez jamais un échange de lancement, mais le commentaire de nobar doit être rappelé: Assurez-vous que les objets que vous échangez ont un échange sans lancement.
Edit 2011-11-06: article intéressant
Dave Abrahams , qui nous a donné les garanties de base / fort / non-retour , a décrit dans un article son expérience sur la sécurisation de l'exception STL:
http://www.boost.org/community/exception_safety.html
Regardez le 7ème point (tests automatisés pour la sécurité des exceptions), où il s'appuie sur des tests unitaires automatisés pour s'assurer que chaque cas est testé. Je suppose que cette partie est une excellente réponse à la question de l'auteur " Pouvez-vous même en être sûr? ".
Edit 2013-05-31: Commentaire de Dionadar
t.integer += 1;
est sans la garantie qu'aucun débordement ne se produira PAS sans exception, et peut en fait invoquer techniquement UB! (Le débordement signé est UB: C ++ 11 5/4 "Si, lors de l'évaluation d'une expression, le résultat n'est pas défini mathématiquement ou n'est pas dans la plage de valeurs représentables pour son type, le comportement n'est pas défini.") Notez que non signé les entiers ne débordent pas, mais effectuent leurs calculs dans une classe d'équivalence modulo 2 ^ # bits.
Dionadar fait référence à la ligne suivante, qui a en effet un comportement indéfini.
t.integer += 1 ; // 1. nothrow/nofail
La solution ici est de vérifier si l'entier est déjà à sa valeur maximale (en utilisant std::numeric_limits<T>::max()
) avant de faire l'ajout.
Mon erreur irait dans la section "Échec normal contre bogue", c'est-à-dire un bogue. Cela n'invalide pas le raisonnement, et cela ne signifie pas que le code d'exception est inutile car impossible à atteindre. Vous ne pouvez pas vous protéger contre l'extinction de l'ordinateur, les bogues du compilateur, ou même vos bogues, ou d'autres erreurs. Vous ne pouvez pas atteindre la perfection, mais vous pouvez vous rapprocher le plus possible.
J'ai corrigé le code en gardant à l'esprit le commentaire de Dionadar.