J'ai toujours été convaincu que si une méthode peut générer une exception, il est imprudent de ne pas protéger cet appel avec un bloc try significatif.
Je viens de poster 'Vous devez TOUJOURS encapsuler les appels qui peuvent essayer, prendre des blocs. 'à cette question et on m'a dit que c'était' un conseil remarquablement mauvais '- j'aimerais comprendre pourquoi.
Une méthode ne doit intercepter une exception que si elle peut la gérer de manière raisonnable.
Sinon, transmettez-le, dans l’espoir qu’une méthode plus élevée dans la pile d’appels puisse la comprendre.
Comme d'autres l'ont noté, il est judicieux de disposer d'un gestionnaire d'exceptions non géré (avec journalisation) au plus haut niveau de la pile d'appels pour garantir que toutes les erreurs fatales sont consignées.
Comme Mitchetautres ont déclaré, vous ne devriez pas attraper une exception que vous ne prévoyez pas de gérer d’une manière ou d’une autre. Vous devez déterminer comment l’application va systématiquement gérer les exceptions lors de sa conception. Cela conduit généralement à avoir des couches de traitement des erreurs basées sur les abstractions - par exemple, vous gérez toutes les erreurs liées au SQL dans votre code d'accès aux données de sorte que la partie de l'application qui interagit avec les objets de domaine ne soit pas exposée au est un DB sous le capot quelque part.
Il y a quelques odeurs de code liées que vous voulez absolument éviter, en plus de "attraper tout partout".
"catch, log, rethrow": si vous souhaitez une journalisation basée sur une portée, écrivez une classe qui émet une instruction de journal dans son destructeur lorsque la pile se déroule à cause d'une exception (ala std::uncaught_exception()
). Tout ce que vous avez à faire est de déclarer une instance de journalisation dans la portée qui vous intéresse et, voila, vous avez une journalisation et aucune logique try
catch
inutile.
_/"attraper, jeter traduit": cela pointe généralement vers un problème d'abstraction. Sauf si vous implémentez une solution fédérée dans laquelle vous traduisez plusieurs exceptions spécifiques en une autre générique, vous avez probablement une couche d'abstraction inutile ... et ne dites pas que "j'en aurai peut-être besoin demain".
"catch, cleanup, rethrow": c'est l'un de mes maux de tête. Si vous en voyez beaucoup, appliquez Resource Acquisition is Initialization techniques et placez la partie nettoyage dans le destructeur d'une instance d'objet janitor.
Je considère que le code jonché de blocs try
/catch
est une bonne cible pour la révision et le refactoring de code. Cela indique que le traitement des exceptions n’est pas bien compris ou que le code est devenu un amœba et a sérieusement besoin d’être refondu.
Parce que la question suivante est "J'ai attrapé une exception, que dois-je faire ensuite?" Que vas-tu faire? Si vous ne faites rien - c'est une erreur de masquage et le programme pourrait "ne pas fonctionner" sans aucune chance de trouver ce qui s'est passé. Vous devez comprendre ce que vous ferez exactement une fois que vous aurez capturé l’exception et seulement si vous le savez.
Herb Sutter a écrit sur ce problème ici . Bien sûr, mérite d'être lu.
Un teaser:
"Écrire du code protégé contre les exceptions, c'est fondamentalement écrire" essayer "et" attraper "aux endroits appropriés." Discuter.
En termes clairs, cette déclaration est le reflet d'une incompréhension fondamentale de la sécurité des exceptions. Les exceptions ne sont qu’une autre forme de rapport d’erreur, et nous savons certainement que l’écriture de code sans erreur ne consiste pas uniquement à vérifier les codes de retour et à gérer les conditions d’erreur.
En fait, il s'avère que la sécurité des exceptions consiste rarement à écrire «essayer» et «attraper» - et plus rarement mieux c'est. En outre, n'oubliez jamais que la sécurité des exceptions affecte la conception d'un code. ce n'est jamais une réflexion après coup qui peut être complétée avec quelques déclarations de capture supplémentaires comme pour un assaisonnement.
Vous n'avez pas besoin de couvrir chaque bloc avec try-catch, car un try-catch peut toujours récupérer les exceptions non gérées lancées dans des fonctions plus en aval de la pile d'appels. Ainsi, plutôt que de donner à toutes les fonctions un try-catch, vous pouvez en avoir un au premier niveau de la logique de votre application. Par exemple, il peut exister une routine de niveau supérieur SaveDocument()
, qui appelle de nombreuses méthodes qui appellent d'autres méthodes, etc. Ces sous-méthodes n'ont pas besoin de leurs propres captures try, car si elles lancent, elles sont toujours capturées par la capture de SaveDocument()
.
C’est bien pour trois raisons: c’est pratique parce que vous n’avez qu’un seul endroit pour signaler une erreur: le ou les blocs catch SaveDocument()
. Il n'est pas nécessaire de répéter cela dans toutes les sous-méthodes, et c'est ce que vous voulez: un seul endroit pour donner à l'utilisateur un diagnostic utile sur quelque chose qui a mal tourné.
Deuxièmement, la sauvegarde est annulée chaque fois qu’une exception est levée. Avec chaque sous-méthode try-catching, si une exception est levée, vous entrez dans le bloc catch de cette méthode, l'exécution laisse la fonction et elle continue entre et SaveDocument()
. Si quelque chose ne va pas, vous voudrez probablement vous arrêter là.
Troisièmement, toutes vos sous-méthodes peuvent supposer que chaque appel réussit. Si un appel échoue, l'exécution saute au bloc catch et le code suivant n'est jamais exécuté. Cela peut rendre votre code beaucoup plus propre. Par exemple, voici avec les codes d'erreur:
int ret = SaveFirstSection();
if (ret == FAILED)
{
/* some diagnostic */
return;
}
ret = SaveSecondSection();
if (ret == FAILED)
{
/* some diagnostic */
return;
}
ret = SaveThirdSection();
if (ret == FAILED)
{
/* some diagnostic */
return;
}
Voici comment cela pourrait être écrit avec des exceptions:
// these throw if failed, caught in SaveDocument's catch
SaveFirstSection();
SaveSecondSection();
SaveThirdSection();
Maintenant, ce qui se passe est beaucoup plus clair.
Notez que le code sécurisé des exceptions peut être plus délicat à écrire d'une autre manière: vous ne voulez pas perdre de mémoire si une exception est levée. Assurez-vous de connaîtreRAII, les conteneurs STL, les pointeurs intelligents et les autres objets libérant leurs ressources dans les destructeurs, car les objets sont toujours détruits avant les exceptions.
Comme indiqué dans d'autres réponses, vous ne devriez intercepter une exception que si vous pouvez effectuer une sorte de traitement d'erreur raisonnable pour elle.
Par exemple, dans la question qui a engendré votre question, l'intervenant demande s'il est prudent d'ignorer les exceptions pour un lexical_cast
d'un entier à une chaîne. Un tel casting ne devrait jamais échouer. Si cela échouait, quelque chose n'allait pas du tout dans le programme. Que pourriez-vous faire pour récupérer dans cette situation? Il est probablement préférable de laisser simplement le programme mourir, car il se trouve dans un état auquel on ne peut faire confiance. Donc, ne pas gérer l'exception peut être la solution la plus sûre.
Si vous manipulez toujours les exceptions immédiatement dans l'appelant d'une méthode qui peut générer une exception, les exceptions deviennent inutiles et vous feriez mieux d'utiliser des codes d'erreur.
Le point essentiel des exceptions est qu’elles ne doivent pas nécessairement être gérées dans toutes les méthodes de la chaîne d’appel.
Le meilleur conseil que j’ai entendu, c’est que vous ne devriez capturer les exceptions qu’à des moments où vous pouvez raisonnablement faire quelque chose au sujet de cette condition exceptionnelle, et que «capturer, consigner et publier» n’est pas une bonne stratégie (même s’il est parfois inévitable dans les bibliothèques).
Je suis d'accord avec l'orientation fondamentale de votre question, qui consiste à traiter autant d'exceptions que possible au niveau le plus bas.
Certaines des réponses existantes vont comme "Vous n'avez pas besoin de gérer l'exception. Quelqu'un d'autre le fera dans la pile." Selon mon expérience, c’est une mauvaise excuse pour ne pas penser à la gestion des exceptions dans le code actuellement développé, ce qui fait de l’exception le problème de quelqu'un d’autre ou plus tard.
Ce problème se développe considérablement dans le développement distribué, où vous devrez peut-être appeler une méthode mise en œuvre par un collègue. Et ensuite, vous devez inspecter une chaîne imbriquée d'appels de méthode pour déterminer pourquoi il/elle vous lance une exception, ce qui aurait pu être traité beaucoup plus facilement avec la méthode imbriquée la plus profonde.
Le conseil que m'avait donné mon professeur d'informatique était le suivant: "Utilisez les blocs Try and Catch uniquement lorsqu'il n'est pas possible de gérer l'erreur avec des moyens standard."
Par exemple, il nous a dit que si un programme rencontrait un problème grave dans un endroit où il était impossible de faire quelque chose comme:
int f()
{
// Do stuff
if (condition == false)
return -1;
return 0;
}
int condition = f();
if (f != 0)
{
// handle error
}
Ensuite, vous devriez utiliser essayer, attraper des blocs. Bien que vous puissiez utiliser des exceptions pour gérer cela, cela n'est généralement pas recommandé car les exceptions coûtent cher en termes de performances.
Si vous souhaitez tester le résultat de chaque fonction, utilisez les codes de retour.
Le but des exceptions est de vous permettre de tester les résultats MOINS souvent. L'idée est de séparer les conditions exceptionnelles (inhabituelles, plus rares) de votre code plus ordinaire. Cela permet au code ordinaire d'être plus propre et plus simple, tout en restant capable de gérer ces conditions exceptionnelles.
Dans un code bien conçu, des fonctions plus profondes peuvent être lancées et des fonctions plus élevées peuvent être capturées. Mais la clé est que beaucoup de fonctions "entre les deux" seront exemptes de la charge de gérer des conditions exceptionnelles. Ils doivent seulement être "exceptionnels safe", ce qui ne signifie pas qu'ils doivent attraper.
Outre les conseils ci-dessus, j'utilise personnellement quelques essais + attrape + lancer; pour la raison suivante:
J'aimerais ajouter à cette discussion que, depuis C++ 11, cela a beaucoup de sens, à condition que chaque catch
blockrethrow
soit l'exception jusqu'au point où elle peut/devrait être gérée. De cette façon, une trace peut être générée. Je pense donc que les avis précédents sont en partie obsolètes.
std::nested_exception
et std::throw_with_nested
Il est décrit sur StackOverflow ici et ici comment y parvenir.
Comme vous pouvez le faire avec n'importe quelle classe d'exception dérivée, vous pouvez ajouter beaucoup d'informations à un tel backtrace! Vous pouvez également consulter mon MWE sur GitHub , où un backtrace ressemblerait à quelque chose comme ceci :
Library API: Exception caught in function 'api_function'
Backtrace:
~/Git/mwe-cpp-exception/src/detail/Library.cpp:17 : library_function failed
~/Git/mwe-cpp-exception/src/detail/Library.cpp:13 : could not open file "nonexistent.txt"
Je me sens obligé d'ajouter une autre réponse bien que celle de Mike Wheat résume assez bien les points principaux. J'y pense comme ça. Lorsque vous avez des méthodes faisant plusieurs choses, vous multipliez la complexité, sans l'ajouter.
En d'autres termes, une méthode encapsulée dans une capture try a deux résultats possibles. Vous avez le résultat sans exception et le résultat avec exception. Lorsque vous utilisez beaucoup de méthodes, cela explose de façon exponentielle au-delà de la compréhension.
De manière exponentielle, car si chaque méthode crée des branches de deux manières différentes, chaque fois que vous appelez une autre méthode, vous quadrillez le nombre précédent de résultats potentiels. Au moment où vous avez appelé cinq méthodes, vous avez au moins 256 résultats possibles. Comparez ceci à not en essayant/attrapez dans chaque méthode et vous n’avez qu’un chemin à suivre.
C'est fondamentalement comment je le regarde. Vous pourriez être tenté de dire que n'importe quel type de branchement fait la même chose, mais essayez/captures est un cas particulier car l'état de l'application devient fondamentalement indéfini.
En bref, essayer/attraper rend le code beaucoup plus difficile à comprendre.
Si vous souhaitez résoudre facilement les problèmes de production intermittente, vous devez envelopper chaque bloc de code dans un bloc try..catch. Cela a essentiellement pour effet d'instrumenter le code dans le but de fournir des informations de débogage étendues vous permettant de déboguer sans un débogueur en production. Les utilisateurs n'ont pas à envoyer de courrier électronique ou à discuter avec le support technique et toutes les informations nécessaires pour résoudre le problème sont disponibles. Il n'est pas nécessaire de reproduire les problèmes.
Pour fonctionner correctement, il doit être associé à une journalisation étendue capable de capturer l'espace de nom/module, le nom de la classe, la méthode, les entrées et le message d'erreur, puis de le stocker dans une base de données afin de pouvoir être agrégé afin de mettre en évidence la méthode qui échoue le plus. fixé en premier.
Les exceptions sont 100 à 1000 fois plus lentes que le code normal et ne doivent jamais être relancées. Ne créez pas non plus d'exception et lancez-la. C'est très distpruotive. Les exceptions sont interceptées afin que le code puisse être corrigé.
Cette technique a été utilisée pour stabiliser rapidement une application buggy dans une entreprise du groupe Fortune 500 développée par 12 développeurs sur 2 ans. À l'aide de cela, j'ai identifié, corrigé, construit des tests et déployé 3 000 correctifs en 4 mois, auquel cas le système ne signalait plus aucune exception, car ils étaient tous traités. Cette moyenne est fixe toutes les 15 minutes en moyenne pendant 4 mois.