Ecrivez-vous (vraiment) du code d’exception?

La gestion des exceptions (EH) semble être la norme actuelle et en effectuant une recherche sur le Web, je ne trouve aucune nouvelle idée ou méthode qui tente de l’améliorer ou de la remplacer (enfin, certaines variantes existent, mais rien de nouveau).

Bien que la plupart des gens semblent l’ignorer ou simplement l’accepter, EH a quelques inconvénients énormes: les exceptions sont invisibles pour le code et cela crée beaucoup, beaucoup de points de sortie possibles. Joel sur le logiciel a écrit un article à ce sujet . La comparaison avec goto correspond parfaitement, cela m’a fait repenser à EH.

J’essaie d’éviter EH et j’utilise simplement des valeurs de retour, des rappels ou tout ce qui convient. Mais lorsque vous devez écrire du code fiable, vous ne pouvez pas ignorer EH ces jours-ci : il commence par le new , qui peut déclencher une exception, au lieu de simplement renvoyer 0 (comme dans l’ancien temps). Cela rend toute ligne de code C ++ vulnérable à une exception. Et puis, plus d’endroits dans le code C ++ fondent des exceptions … std lib le fait, et ainsi de suite.

Cela ressemble à marcher sur des terrains fragiles .. Alors, maintenant, nous sums obligés de faire attention aux exceptions!

Mais c’est dur, c’est vraiment difficile. Vous devez apprendre à écrire du code de sécurité d’exception, et même si vous en avez une certaine expérience, il vous faudra toujours vérifier toute ligne de code pour être sûr! Ou vous commencez à mettre des blocs try / catch partout, ce qui encombre le code jusqu’à ce qu’il devienne illisible.

EH a remplacé l’ancienne approche déterministe propre (valeurs de retour ..), qui présentait des inconvénients simples mais compréhensibles et faciles à résoudre avec une approche qui crée de nombreux points de sortie possibles dans votre code, et si vous commencez à écrire du code sont obligés de le faire à un moment donné, puis il crée même une multitude de chemins à travers votre code (code dans les blocs catch, pensez à un programme serveur où vous avez besoin d’outils de journalisation autres que std :: cerr ..). EH a des avantages, mais ce n’est pas le cas.

Mes questions réelles:

  • Est-ce que vous écrivez vraiment du code de sécurité d’exception?
  • Etes-vous sûr que votre dernier code “production ready” est protégé contre les exceptions?
  • Pouvez-vous être sûr que c’est le cas?
  • Connaissez-vous et / ou utilisez-vous réellement des alternatives qui fonctionnent?

Votre question fait l’affirmation suivante: “L’écriture de code sans exception est très difficile”. Je vais d’abord répondre à vos questions, puis répondre à la question cachée derrière eux.

Répondre à des questions

Est-ce que vous écrivez vraiment du code de sécurité 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 ++ (absence de sémantique RAII), mais je digresse: ceci est une question C ++.

Il est en effet nécessaire lorsque vous devez travailler avec un code STL ou Boost. Par exemple, les threads C ++ ( boost::thread ou std::thread ) lancent une exception pour quitter correctement.

Etes-vous sûr que votre dernier code “production ready” est protégé contre les exceptions?

Pouvez-vous être sûr que c’est le cas?

Ecrire du code sans exception est comme écrire un code sans bogue.

Vous ne pouvez pas être sûr à 100% que votre code est protégé contre les exceptions. Mais alors, vous vous efforcez de le faire, en utilisant des modèles bien connus et en évitant les anti-patrons 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.-à-d. Que vous devrez revenir à C et éviter les bibliothèques C ++, ainsi que les sursockets externes comme Windows SEH).

Ecriture du code de sécurité d’exception

Pour écrire un code de sécurité 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 lancer une exception, mais l’atsortingbution d’un élément intégré (par exemple, un int ou un pointeur) n’échouera pas. Un swap n’échouera jamais (ne jamais écrire un swap 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 toutes vos fonctions:

  1. none : Votre code ne devrait jamais offrir cela. Ce code va tout fuir et tomber en panne à la toute première exception lancée.
  2. basic : C’est la garantie que vous devez au moins offrir, c’est-à-dire que si une exception est lancée, aucune fuite de ressources et tous les objects sont encore entiers
  3. strong : le traitement réussira ou lancera 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 une puissance transactionnelle à C ++)
  4. nothrow / nofail : le traitement réussira.

Exemple de code

Le code suivant semble être un C ++ correct, mais en vérité, il offre la garantie “none”, et donc, ce n’est pas correct:

 void doSomething(T & t) { if(std::numeric_limits::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 type d’parsing en tête.

La garantie la plus basse offerte est basique, mais ensuite, le classement de chaque instruction rend la fonction entière “none”, car si 3. jette, x va fuir.

La première chose à faire serait de rendre la fonction “basic”, c’est-à-dire de mettre x dans un pointeur intelligent jusqu’à ce que la liste en détienne la sécurité:

 void doSomething(T & t) { if(std::numeric_limits::max() > t.integer) // 1. nothrow/nofail t.integer += 1 ; // 1'. nothrow/nofail std::auto_ptr 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 } 

Maintenant, notre code offre une garantie “de base”. Rien ne va fuir et tous les objects seront dans un état correct. Mais nous pourrions offrir plus, c’est-à-dire la garantie forte. C’est là que cela peut devenir coûteux, et c’est pourquoi tous les codes C ++ ne sont pas solides. Essayons:

 void doSomething(T & t) { // we create "x" std::auto_ptr 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::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 définissant X à sa juste valeur. Si une opération échoue, alors t n’est pas modifié, alors l’opération 1 à 3 peut être considérée comme “forte”: si quelque chose se lève, 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 alors, t est toujours l’original. Nous offrons toujours la garantie forte.

Ensuite, nous échangeons t et t2 . Les opérations de swap devraient être nothrow en C ++, alors espérons que le swap que vous avez écrit pour T est nothrow (si ce n’est pas le cas, réécrivez-le pour qu’il soit nothrow).

Donc, si on arrive à 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, alors t a toujours sa valeur d’origine.

Maintenant, offrir une garantie forte pourrait coûter très cher, alors ne vous efforcez pas d’offrir une garantie solide à tout votre code, mais si vous pouvez le faire sans coût (et l’inclusion C ++ et autres optimisations peuvent rendre tout le code sans coût) , alors fais-le. La fonction utilisateur 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 sauvegarde pas la garantie (dans mon code, j’offre la garantie en tant que tag dowarge @warning), ce qui est plutôt sortingste, mais cela ne doit pas vous empêcher d’essayer d’écrire du code sécurisé.

É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 proposées par un code sans bug. Mais alors, dans n’importe quelle langue, l’appel d’une fonction suppose que la fonction est exempte de bogues. Aucun code sain ne se protège contre l’éventualité d’un bug. Écrivez le code le mieux possible, puis offrez la garantie en supposant qu’il n’y a pas de bogue. Et s’il y a un bug, corrigez-le.

Les exceptions concernent les erreurs de traitement exceptionnelles et non les bogues de code.

Derniers mots

Maintenant, la question est “Est-ce que cela en vaut la peine?”.

Bien sûr que oui. Avoir une fonction “nothrow / no-fail” en sachant que la fonction ne va pas échouer est une aubaine. On peut dire la même chose pour une fonction “forte”, qui vous permet d’écrire du code avec une sémantique transactionnelle, comme des bases de données, avec des fonctionnalités de validation / restauration, la validation étant l’exécution normale du code.

Ensuite, le “basique” est la moindre garantie que vous devriez offrir. C ++ est un langage très fort, avec ses étendues, qui vous permet d’éviter toute fuite de ressources (ce que le ramasse-miettes trouverait difficile à offrir pour la firebase database, les descripteurs de connexion ou de fichiers).

Donc, autant que je le vois, cela en vaut la peine.

Edit 29-01-2010: À propos des swaps sans jet

nobar a fait un commentaire qui, je crois, est très pertinent, car il fait partie de “Comment écrivez-vous le code de sécurité d’exception”:

  • [moi] Un échange ne manquera jamais (n’écrit même pas un échange de jet)
  • [nobar] Ceci est une bonne recommandation pour les fonctions swap() . Il convient de noter, cependant, que std::swap() peut échouer en fonction des opérations qu’il utilise en interne

std::swap par défaut fera des copies et des affectations qui, pour certains objects, peuvent être lancées. Ainsi, le swap par défaut peut être utilisé, que ce soit pour vos classes ou même pour les classes STL. En ce qui concerne le standard C ++, l’opération de swap pour vector , deque et list ne sera pas lancée, alors que pour le map si le foncteur de comparaison peut lancer une construction de copie (voir Langage de programmation C ++, édition spéciale, annexe E). , E.4.3.Swap ).

En regardant l’implémentation de 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 lancer dans ce dernier cas.

Ainsi, le texte original est toujours valable: n’écrivez jamais un échange de lancers, mais rappelez-vous que le commentaire de nobar: Assurez-vous que les objects que vous échangez ont un échange non-lanceur.

Modifier 2011-11-06: Article intéressant

Dave Abrahams , qui nous a donné les garanties de base / forte / nothrow , a décrit dans un article son expérience de la sécurité de l’exception STL:

http://www.boost.org/community/exception_safety.html

Regardez le septième point (Test automatisé 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 à l’auteur de la question ” Pouvez-vous être sûr que c’est le cas? “.

Modifier 2013-05-31: Commentaire de dionadar

t.integer += 1; est sans la garantie que le débordement ne se produira pas pas l’exception de sécurité, et en fait peut techniquement invoquer 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 ne se situe pas dans la plage des valeurs représentables pour son type, le comportement n’est pas défini.” integer ne déborde pas, mais effectue ses calculs dans une classe d’équivalence modulo 2 ^ # bits.

Dionadar se réfère à 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::max() ) avant de faire l’addition.

Mon erreur irait dans la section “Echec normal vs. bug”, c’est-à-dire un bogue. Cela n’invalide pas le raisonnement, et cela ne signifie pas que le code sécurisé des exceptions est inutile car impossible à atteindre. Vous ne pouvez pas vous protéger contre la mise hors tension de l’ordinateur, les bogues du compilateur ou même vos bogues ou autres erreurs. Vous ne pouvez pas atteindre la perfection, mais vous pouvez essayer de vous rapprocher le plus possible.

J’ai corrigé le code avec le commentaire de Dionadar en tête.

L’écriture de code sécurisé en C ++ ne consiste pas à utiliser beaucoup de blocs try {} catch {}. Il s’agit de documenter le type de garanties fournies par votre code.

Je recommande de lire la série Guru Of The Week de Herb Sutter, en particulier les versions 59, 60 et 61.

Pour résumer, il existe trois niveaux de sécurité des exceptions que vous pouvez fournir:

  • De base: lorsque votre code déclenche une exception, votre code ne génère pas de fuite de ressources et les objects restnt destructibles.
  • Strong: lorsque votre code déclenche une exception, l’état de l’application rest inchangé.
  • No throw: votre code ne lève jamais d’exceptions.

Personnellement, j’ai découvert ces articles assez tard, une grande partie de mon code C ++ n’est certainement pas protégée contre les exceptions.

Certains d’entre nous utilisent les exceptions depuis plus de 20 ans. PL / I les a, par exemple. La prémisse selon laquelle ils sont une technologie nouvelle et dangereuse me semble discutable.

Tout d’abord (comme Neil l’a indiqué), SEH est la gestion des exceptions structurées de Microsoft. Il est similaire mais pas identique au traitement des exceptions en C ++. En fait, vous devez activer la gestion des exceptions C ++ si vous le souhaitez dans Visual Studio – le comportement par défaut ne garantit pas que les objects locaux sont détruits dans tous les cas! Dans les deux cas, la gestion des exceptions n’est pas vraiment plus difficile, elle est simplement différente .

Maintenant, pour vos questions réelles.

Est-ce que vous écrivez vraiment du code de sécurité d’exception?

Oui. Je m’efforce d’obtenir du code d’exception dans tous les cas. J’évangélise en utilisant les techniques RAII pour accéder aux ressources (ex . boost::shared_ptr pour la mémoire, boost::lock_guard pour le locking). En général, une utilisation cohérente des techniques RAII et de protection de la scope rendra le code de sécurité des exceptions beaucoup plus facile à écrire. L’astuce consiste à apprendre ce qui existe et comment l’appliquer.

Etes-vous sûr que votre dernier code “production ready” est protégé contre les exceptions?

Non, c’est aussi sûr que ça. Je peux dire que je n’ai pas vu de défaut de processus dû à une exception sur plusieurs années d’activité 24/7. Je ne m’attends pas à un code parfait, juste un code bien écrit. En plus de fournir une sécurité d’exception, les techniques ci-dessus garantissent l’exactitude d’une manière quasiment impossible à obtenir avec catch blocs try / catch . Si vous attrapez tout ce qui se trouve dans la scope de votre contrôle supérieur (thread, processus, etc.), vous pouvez être sûr que vous continuerez à exécuter les exceptions (la plupart du temps ). Les mêmes techniques vous aideront également à continuer à fonctionner correctement en présence d’exceptions sans blocs d’ try / catch partout .

Pouvez-vous être sûr que c’est le cas?

Oui. Vous pouvez être sûr par un audit approfondi du code, mais personne ne le fait vraiment? Des révisions de code régulières et des développeurs attentifs consortingbuent grandement à y parvenir.

Connaissez-vous et / ou utilisez-vous réellement des alternatives qui fonctionnent?

J’ai essayé quelques variantes au fil des ans, comme le codage des états dans les bits supérieurs (ala HRESULT s ) ou cet horrible setjmp() ... longjmp() hack. Ces deux éléments se décomposent dans la pratique, mais de manière complètement différente.


En fin de compte, si vous prenez l’habitude d’appliquer quelques techniques et que vous réfléchissez soigneusement à la manière de faire quelque chose en réponse à une exception, vous vous retrouverez avec un code très lisible et sécurisé. Vous pouvez résumer cela en suivant ces règles:

  • Vous voulez seulement voir try / catch quand vous pouvez faire quelque chose pour une exception spécifique
  • Vous ne voulez presque jamais voir un new brut ou delete dans le code
  • Évitez std::sprintf snprintf , snprintf et les tableaux en général – utilisez std::ossortingngstream pour le formatage et remplacez les tableaux par std::vector et std::ssortingng
  • En cas de doute, recherchez des fonctionnalités dans Boost ou STL avant de lancer votre propre

Je ne peux que vous recommander d’apprendre à utiliser correctement les exceptions et d’oublier les codes de résultat si vous envisagez d’écrire en C ++. Si vous souhaitez éviter les exceptions, vous pouvez envisager d’écrire dans une autre langue qui ne les contient pas ou les rend sûres . Si vous voulez vraiment apprendre à utiliser pleinement C ++, lisez quelques livres de Herb Sutter , Nicolai Josuttis et Scott Meyers .

Il n’est pas possible d’écrire du code sans exception en supposant que “n’importe quelle ligne peut lancer”. La conception d’un code de sécurité d’exception dépend de certains contrats / garanties que vous êtes censé attendre, observer, suivre et implémenter dans votre code. Il est absolument nécessaire d’avoir un code garanti pour ne jamais lancer. Il existe d’autres types de garanties d’exception.

En d’autres termes, la création d’un code sûr pour les exceptions est en grande partie une question de conception de programme, et pas uniquement de simple codage .

  • Est-ce que vous écrivez vraiment du code de sécurité d’exception?

Eh bien, j’ai certainement l’intention de.

  • Etes-vous sûr que votre dernier code “production ready” est protégé contre les exceptions?

Je suis sûr que mes serveurs 24/7 construits en utilisant des exceptions fonctionnent 24 heures sur 24 et ne fuient pas la mémoire.

  • Pouvez-vous être sûr que c’est le cas?

Il est très difficile de s’assurer que tout code est correct. En règle générale, on ne peut aller que par les résultats

  • Connaissez-vous et / ou utilisez-vous réellement des alternatives qui fonctionnent?

Non. L’utilisation des exceptions est plus propre et plus facile que toutes les alternatives que j’ai utilisées au cours des 30 dernières années en programmation.

En laissant de côté la confusion entre les exceptions SEH et C ++, vous devez savoir que des exceptions peuvent être générées à tout moment et écrire votre code en tenant compte de cela. Le besoin de sécurité des exceptions est en grande partie responsable de l’utilisation de RAII, des pointeurs intelligents et d’autres techniques C ++ modernes.

Si vous suivez les modèles bien établis, écrire du code exempt d’exception n’est pas particulièrement difficile, et en fait, il est plus facile d’écrire du code qui gère correctement les retours d’erreur dans tous les cas.

EH est bon, en général. Mais l’implémentation de C ++ n’est pas très conviviale, car il est difficile de déterminer la couverture de vos exceptions. Java, par exemple, rend cela facile, le compilateur aura tendance à échouer si vous ne gérez pas les exceptions possibles.

J’aime beaucoup travailler avec Eclipse et Java (nouveau en Java), car cela génère des erreurs dans l’éditeur si vous manquez un gestionnaire EH. Cela rend les choses beaucoup plus difficiles à oublier pour gérer une exception …

De plus, avec les outils IDE, il ajoute automatiquement le bloc try / catch ou un autre bloc catch.

Certains d’entre nous préfèrent des langages comme Java qui nous obligent à déclarer toutes les exceptions levées par les méthodes, au lieu de les rendre invisibles comme en C ++ et en C #.

Lorsque cela est fait correctement, les exceptions sont supérieures aux codes de retour d’erreur, si pour une autre raison que vous n’avez pas à propager les échecs à la chaîne d’appels manuellement.

Cela étant dit, la programmation de la bibliothèque d’API de bas niveau devrait probablement éviter la gestion des exceptions et s’en tenir aux codes de retour d’erreur.

D’après mon expérience, il est difficile d’écrire du code de gestion des exceptions propre en C ++. Je new(nothrow) par utiliser new(nothrow) beaucoup.

J’essaie le mieux pour écrire du code sécurisé, oui.

Cela signifie que je prends soin de garder un œil sur les lignes qui peuvent être lancées. Tout le monde ne peut pas, et il est extrêmement important de garder cela à l’esprit. La clé est vraiment de réfléchir et de concevoir votre code pour satisfaire les garanties d’exception définies dans la norme.

Cette opération peut-elle être écrite pour fournir la garantie d’exception forte? Dois-je me contenter de la base? Quelles lignes peuvent émettre des exceptions, et comment puis-je m’assurer que si elles le font, elles ne corrompent pas l’object?

  • Est-ce que vous écrivez vraiment du code de sécurité d’exception? [Il n’y a rien comme ça. Les exceptions sont un bouclier papier aux erreurs, sauf si vous avez un environnement géré. Cela s’applique aux trois premières questions.]

  • Connaissez-vous et / ou utilisez-vous réellement des alternatives qui fonctionnent? [Alternative à quoi? Le problème ici est que les gens ne séparent pas les erreurs réelles du fonctionnement normal du programme. Si c’est un fonctionnement normal du programme (c’est-à-dire un fichier introuvable), ce n’est pas vraiment une gestion des erreurs. S’il s’agit d’une erreur réelle, il n’y a aucun moyen de le “gérer” ou ce n’est pas une erreur réelle. Votre objective est de découvrir ce qui a mal tourné et d’arrêter la feuille de calcul et d’enregistrer une erreur, de redémarrer le pilote sur votre grid-pain ou de prier pour que le jetfighter puisse continuer à voler.

Beaucoup (je dirais même le plus) les gens font.

Ce qui est vraiment important à propos des exceptions, c’est que si vous n’écrivez aucun code de manipulation, le résultat est parfaitement sûr et correct. Trop désireux de paniquer, mais en sécurité.

Vous devez faire des erreurs dans les gestionnaires pour obtenir quelque chose de dangereux, et seulement catch (…) {} sera comparable à ignorer le code d’erreur.