La FAQ d'exception isocpp.org indique
N'utilisez pas throw pour indiquer une erreur de codage lors de l'utilisation d'une fonction. Utilisez assert ou un autre mécanisme pour envoyer le processus dans un débogueur ou pour bloquer le processus et collecter le vidage sur incident pour le développeur à déboguer.
D'un autre côté, la bibliothèque standard définit std :: logic_error et tous ses dérivés, qui me semblent être censés gérer, entre autres, les erreurs de programmation. Passer une chaîne vide à std :: stof (lancera invalid_argument) n'est-il pas une erreur de programmation? Passer une chaîne contenant des caractères différents de «1» / «0» à std :: bitset (lancera invalid_argument) n'est-il pas une erreur de programmation? L'appel de std :: bitset :: set avec un index invalide (lancera out_of_range) n'est-il pas une erreur de programmation? Si ce n'est pas le cas, alors quelle est une erreur de programmation que l'on pourrait tester? Le constructeur basé sur une chaîne std :: bitset n'existe que depuis C ++ 11, il aurait donc dû être conçu avec une utilisation idiomatique des exceptions à l'esprit. D'un autre côté, des gens m'ont dit que logic_error ne devait pas du tout être utilisé.
Une autre règle qui revient fréquemment avec des exceptions est "n'utilisez les exceptions que dans des circonstances exceptionnelles". Mais comment une fonction de bibliothèque est-elle censée savoir quelles circonstances sont exceptionnelles? Pour certains programmes, le fait de ne pas pouvoir ouvrir un fichier peut être exceptionnel. Pour d'autres, l'impossibilité d'allouer de la mémoire n'est pas exceptionnelle. Et il y a des centaines de cas entre les deux. Impossible de créer une socket? Impossible de se connecter ou d'écrire des données sur un socket ou un fichier? Impossible d'analyser l'entrée? Cela pourrait être exceptionnel, peut-être pas. La fonction elle-même ne peut certainement pas généralement le savoir, elle n'a aucune idée dans quel type de contexte elle est appelée.
Alors, comment suis-je censé décider si je dois utiliser des exceptions ou non pour une fonction particulière? Il me semble que le seul moyen réellement cohérent est de les utiliser pour toutes les manipulations d'erreurs, ou pour rien. Et si j'utilise la bibliothèque standard, ce choix a été fait pour moi.
la source
Réponses:
Tout d'abord, je me sens obligé de souligner que
std::exception
et ses enfants ont été conçus il y a longtemps. Il existe un certain nombre de pièces qui seraient probablement (presque certainement) différentes si elles étaient conçues aujourd'hui.Ne vous méprenez pas: il y a des parties de la conception qui ont assez bien fonctionné, et sont de très bons exemples de la façon de concevoir une hiérarchie d'exceptions pour C ++ (par exemple, le fait que, contrairement à la plupart des autres classes, ils partagent tous un racine commune).
En regardant spécifiquement
logic_error
, nous avons une énigme. D'une part, si vous avez un choix raisonnable en la matière, le conseil que vous avez cité est juste: il est généralement préférable d'échouer aussi rapidement et bruyamment que possible afin qu'il puisse être débogué et corrigé.Pour le meilleur ou pour le pire, cependant, il est difficile de définir la bibliothèque standard autour de ce que vous devez généralement faire. S'il les définissait pour quitter le programme (par exemple, appeler
abort()
) lorsque des données incorrectes étaient fournies, ce serait toujours ce qui se passerait dans ces circonstances - et il y a en fait un certain nombre de circonstances dans lesquelles ce n'est probablement pas vraiment la bonne chose à faire , au moins dans le code déployé.Cela s'appliquerait au code avec des exigences en temps réel (au moins douces) et une pénalité minimale pour une sortie incorrecte. Par exemple, envisagez un programme de chat. S'il décode des données vocales et obtient des entrées incorrectes, il est probable qu'un utilisateur sera beaucoup plus heureux de vivre avec une milliseconde de statique en sortie qu'un programme qui s'arrête complètement. De même, lors de la lecture vidéo, il peut être plus acceptable de vivre avec la production de valeurs erronées pour certains pixels pour une ou deux images que de quitter sommairement le programme car le flux d'entrée a été corrompu.
Quant à savoir s'il faut utiliser des exceptions pour signaler certains types d'erreurs: vous avez raison - la même opération peut être considérée comme une exception ou non, selon la façon dont elle est utilisée.
D'un autre côté, vous avez également tort - l'utilisation de la bibliothèque standard ne vous force pas (nécessairement) à prendre cette décision. Dans le cas de l'ouverture d'un fichier, vous utiliseriez normalement un iostream. Les Iostreams ne sont pas non plus exactement la dernière et la meilleure conception, mais dans ce cas, ils font les choses correctement: ils vous permettent de définir un mode d'erreur, afin que vous puissiez contrôler si l'échec de l'ouverture d'un fichier entraîne une exception ou non. Donc, si vous avez un fichier qui est vraiment nécessaire pour votre application, et si vous ne l'ouvrez pas, vous devez prendre des mesures correctives sérieuses, vous pouvez lui faire lever une exception s'il ne parvient pas à ouvrir ce fichier. Pour la plupart des fichiers que vous essayerez d'ouvrir, s'ils n'existent pas ou ne sont pas accessibles, ils échoueront (c'est la valeur par défaut).
Quant à la façon dont vous décidez: je ne pense pas qu'il y ait de réponse facile. Pour le meilleur ou pour le pire, les «circonstances exceptionnelles» ne sont pas toujours faciles à mesurer. Bien qu'il y ait certainement des cas faciles à décider qui doivent être [non] exceptionnels, il y a (et il y aura probablement toujours) des cas où cela peut être remis en question, ou nécessite une connaissance du contexte qui est en dehors du domaine de la fonction en question. Pour des cas comme celui-ci, il peut au moins être utile d'envisager une conception à peu près similaire à cette partie des iostreams, où l'utilisateur peut décider si l'échec entraîne la levée ou non d'une exception. Alternativement, il est tout à fait possible d'avoir deux ensembles distincts de fonctions (ou classes, etc.), dont l'un lèvera des exceptions pour indiquer l'échec, l'autre utilisant d'autres moyens. Si vous suivez cette voie,
la source
Vous ne le croyez peut-être pas, mais différents codeurs C ++ ne sont pas d'accord. C'est pourquoi la FAQ dit une chose mais la bibliothèque standard n'est pas d'accord.
La FAQ préconise un plantage car cela sera plus facile à déboguer. Si vous vous plantez et obtenez un vidage de mémoire, vous aurez l'état exact de votre application. Si vous jetez une exception, vous perdrez beaucoup de cet état.
La bibliothèque standard prend la théorie selon laquelle donner au codeur la capacité d'attraper et de gérer éventuellement l'erreur est plus important que le débogage.
L'idée ici est que si votre fonction ne sait pas si la situation est exceptionnelle ou non, elle ne doit pas lever d'exception. Il devrait retourner un état d'erreur via un autre mécanisme. Une fois qu'il atteint un point dans le programme où il sait que l'état est exceptionnel, il doit lever l'exception.
Mais cela a son propre problème. Si un état d'erreur est renvoyé par une fonction, vous pourriez ne pas vous souvenir de le vérifier et l'erreur passera silencieusement. Cela amène certaines personnes à abandonner les exceptions qui sont une règle exceptionnelle en faveur de la levée d'exceptions pour tout type d'état d'erreur.
Dans l'ensemble, le point clé est que différentes personnes ont des idées différentes sur le moment de lever les exceptions. Vous n'allez pas trouver une seule idée cohérente. Même si certains voudront affirmer dogmatiquement que telle ou telle est la bonne façon de gérer les exceptions, il n'y a pas de théorie unique convenue.
Vous pouvez lever des exceptions:
et trouver quelqu'un sur Internet qui est d'accord avec vous. Vous devrez adopter le style qui vous convient.
la source
Beaucoup d'autres bonnes réponses ont été écrites, je veux juste ajouter un petit point.
La réponse traditionnelle, en particulier lorsque la FAQ ISO C ++ a été écrite, compare principalement «l'exception C ++» et le «code retour de style C». Une troisième option, "renvoyer un certain type de valeur composite, par exemple un
struct
ouunion
, ou de nos jours,boost::variant
ou le (proposé)std::expected
, n'est pas considérée.Avant C ++ 11, l'option "renvoyer un type composite" était généralement très faible. Parce qu'il n'y avait pas de sémantique de mouvement, la copie de choses dans et hors d'une structure était potentiellement très coûteuse. À ce stade du langage, il était extrêmement important de styliser votre code vers RVO afin d'obtenir les meilleures performances. Les exceptions étaient comme un moyen facile de renvoyer efficacement un type composite, sinon ce serait assez difficile.
IMO, après C ++ 11, cette option "retourner une union discriminée", similaire à l'idiome
Result<T, E>
utilisé dans Rust de nos jours, devrait être privilégiée plus souvent dans le code C ++. Parfois, c'est vraiment un style plus simple et plus pratique pour indiquer les erreurs. À quelques exceptions près, il y a toujours cette possibilité que des fonctions qui n'ont pas été lancées avant puissent soudainement lancer après un refactor, et les programmeurs ne documentent pas toujours bien ce genre de choses. Lorsque l'erreur est indiquée comme faisant partie de la valeur de retour dans une union discriminée, cela réduit considérablement le risque que le programmeur ignore simplement le code d'erreur, ce qui est la critique habituelle de la gestion des erreurs de style C.Fonctionne généralement un
Result<T, E>
peu comme boost facultatif. Vous pouvez tester, en utilisantoperator bool
, s'il s'agit d'une valeur ou d'une erreur. Et puis utilisez sayoperator *
pour accéder à la valeur, ou une autre fonction "get". Habituellement, cet accès n'est pas contrôlé, pour la vitesse. Mais vous pouvez faire en sorte que dans une version de débogage, l'accès soit vérifié et une assertion s'assure qu'il existe réellement une valeur et non une erreur. De cette façon, toute personne qui ne vérifie pas correctement les erreurs obtiendra une affirmation ferme plutôt qu'un problème plus insidieux.Un avantage supplémentaire est que, contrairement aux exceptions où s'il n'est pas capturé, il parcourt la pile à une distance arbitraire, avec ce style, lorsqu'une fonction commence à signaler une erreur là où elle ne l'était pas auparavant, vous ne pouvez pas compiler à moins que le le code est modifié pour le gérer. Cela rend les problèmes plus forts - le problème traditionnel de «l'exception non capturée» ressemble plus à une erreur de compilation qu'à une erreur d'exécution.
Je suis devenu un grand fan de ce style. Habituellement, j'utilise de nos jours ceci ou des exceptions. Mais j'essaie de limiter les exceptions aux problèmes majeurs. Pour quelque chose comme une erreur d'analyse, j'essaie de revenir
expected<T>
par exemple. Des choses commestd::stoi
etboost::lexical_cast
qui lèvent une exception C ++ en cas de problème relativement mineur "la chaîne n'a pas pu être convertie en nombre" me semblent de très mauvais goût de nos jours.la source
std::expected
est toujours une proposition non acceptée, non?exception_ptr
, ou voulez-vous simplement utiliser un type de structure ou quelque chose comme ça.[[nodiscard]] attribute
sera utile pour cette approche de gestion des erreurs car elle garantit que vous n'ignorez pas simplement le résultat de l'erreur par accident.except_ptr
), il fallait lever une exception en interne. Personnellement, je pense qu'un tel outil devrait fonctionner complètement indépendamment des exécutions. Juste une remarque.Il s'agit d'un problème hautement subjectif, car il fait partie de la conception. Et parce que le design est fondamentalement de l'art, je préfère discuter de ces choses au lieu de débattre (je ne dis pas que vous débattez).
Pour moi, les cas exceptionnels sont de deux types - ceux qui concernent les ressources et ceux qui traitent des opérations critiques. Ce qui peut être considéré comme critique dépend du problème à résoudre et, dans de nombreux cas, du point de vue du programmeur.
L'échec à acquérir des ressources est un candidat idéal pour lever des exceptions. La ressource peut être de la mémoire, un fichier, une connexion réseau ou tout autre élément en fonction de votre problème et de votre plate-forme. Maintenant, l'échec de la libération d'une ressource justifie-t-il une exception? Eh bien, cela dépend encore une fois. Je n'ai rien fait où la libération de mémoire a échoué, donc je ne suis pas sûr de ce scénario. Cependant, la suppression de fichiers dans le cadre de la libération des ressources peut échouer, et a échoué pour moi, et cet échec est généralement lié à un autre processus l'ayant maintenu ouvert dans une application multi-processus. Je suppose que d'autres ressources pourraient échouer lors de la publication comme un fichier pourrait le faire, et c'est généralement une faille de conception qui provoque ce problème, il serait donc préférable de le corriger que de lancer une exception.
Vient ensuite la mise à jour des ressources. Ce point est, du moins pour moi, étroitement lié à l'aspect opérationnel critique de l'application. Imaginez une
Employee
classe avec une fonctionUpdateDetails(std::string&)
qui modifie les détails en fonction de la chaîne séparée par des virgules donnée. Semblable à l'échec de la libération de la mémoire, j'ai du mal à imaginer l'échec de l'affectation des valeurs des variables membres en raison de mon manque d'expérience dans ces domaines où cela pourrait se produire. Cependant, une fonction comme celleUpdateDetailsAndUpdateFile(std::string&)
qui fait comme son nom l'indique doit échouer. C'est ce que j'appelle une opération critique.Maintenant, vous devez voir si l'opération dite critique mérite une exception. Je veux dire, la mise à jour du fichier se fait-elle à la fin, comme dans le destructeur, ou est-ce simplement un appel paranoïaque effectué après chaque mise à jour? Existe-t-il un mécanisme de secours qui écrit régulièrement des objets non écrits? Ce que je dis, c'est que vous devez évaluer la criticité de l'opération.
De toute évidence, il existe de nombreuses opérations critiques qui ne sont pas liées aux ressources. Si la
UpdateDetails()
donnée est incorrecte, elle ne mettra pas à jour les détails et l'échec doit être signalé, vous lèverez donc une exception ici. Mais imaginez une fonction commeGiveRaise()
. Maintenant, si ledit employé a de la chance d'avoir un patron aux cheveux pointus et ne recevra pas d'augmentation (en termes de programmation, la valeur de certaines variables empêche cela de se produire), la fonction a essentiellement échoué. Souhaitez-vous lever une exception ici? Ce que je dis, c'est qu'il faut évaluer la nécessité d'une exception.Pour moi, la cohérence est au niveau de mon approche du design plutôt que de la convivialité de mes cours. Ce que je veux dire, c'est que je ne pense pas en termes de «toutes les fonctions Get doivent le faire et toutes les fonctions de mise à jour doivent le faire», mais voyez si une fonction particulière fait appel à une certaine idée dans mon approche. À première vue, les cours pourraient avoir l'air d'une sorte de «hasard», mais chaque fois que les utilisateurs (principalement des collègues d'autres équipes) râleront ou poseront des questions à ce sujet, je m'expliquerai et ils semblent satisfaits.
Je vois beaucoup de gens qui remplacent essentiellement les valeurs de retour par des exceptions parce qu'ils utilisent C ++ et non C, et que cela donne une `` bonne séparation de la gestion des erreurs '', etc. et m'encourage à arrêter de `` mélanger '' les langues, etc. Je reste généralement à l'écart de ces gens.
la source
Premièrement, comme d'autres l'ont déclaré, les choses ne sont pas aussi claires en C ++, à mon humble avis, principalement parce que les exigences et les contraintes sont un peu plus variées en C ++ que dans d'autres langages, en particulier. C # et Java, qui ont des problèmes d'exception "similaires".
Je vais exposer sur l'exemple std :: stof:
Le contrat de base , comme je le vois, de cette fonction est qu'elle essaie de convertir son argument en un flottant, et tout échec à le faire est signalé par une exception. Les deux exceptions possibles sont dérivées,
logic_error
mais pas dans le sens d'erreur de programmeur, mais dans le sens de "l'entrée ne peut jamais être convertie en flottant".Ici, on peut dire que a
logic_error
est utilisé pour indiquer que, étant donné que l'entrée (runtime), c'est toujours une erreur d'essayer de la convertir - mais c'est le travail de la fonction de le déterminer et de vous le dire (via une exception).Note latérale: Dans cette vue, un
runtime_error
pourrait être considéré comme quelque chose qui, étant donné la même entrée à une fonction, pourrait théoriquement réussir pour différentes exécutions. (par exemple, une opération sur fichier, accès DB, etc.)Note supplémentaire: La bibliothèque d'expressions régulières C ++ a choisi de dériver son erreur
runtime_error
bien qu'il existe des cas où elle pourrait être classée de la même manière qu'ici (modèle d'expression régulière non valide).Cela montre juste, à mon humble avis, que le regroupement en
logic_
ouruntime_
erreur est assez flou en C ++ et n'aide pas vraiment beaucoup dans le cas général (*) - si vous devez gérer des erreurs spécifiques, vous devez probablement rattraper plus bas que les deux.(*): Cela ne veut pas dire qu'un seul morceau de code ne doit pas être cohérent, mais si vous jetez
runtime_
oulogic_
oucustom_
somethings est vraiment pas si important que cela, je pense.Pour commenter les deux
stof
etbitset
:Les deux fonctions prennent des chaînes comme argument, et dans les deux cas, c'est:
Cette déclaration a, à mon humble avis, deux racines:
Performances : si une fonction est appelée dans un chemin critique et que le cas "exceptionnel" n'est pas exceptionnel, c'est-à-dire qu'un nombre important de passes impliquera de lever une exception, alors payer à chaque fois pour la machine de déroulement d'exception n'a pas de sens et peut être trop lent.
Localité de la gestion des erreurs : si une fonction est invoquée et que l'exception est immédiatement interceptée et traitée, il est inutile de lancer une exception, car la gestion des erreurs sera plus détaillée avec le
catch
qu'avec avecif
.Exemple:
Voici où des fonctions comme
TryParse
vsParse
entrent en jeu: une version pour quand le code local attend que la chaîne analysée soit valide, une version lorsque le code local suppose qu'il est réellement prévu (c'est-à-dire non exceptionnel) que l'analyse échoue.En effet,
stof
est juste (défini comme) un wrapperstrtof
, donc si vous ne voulez pas d'exceptions, utilisez-le.À mon humble avis, vous avez deux cas:
Fonction de type "bibliothèque" (réutilisée souvent dans des contextes différents): vous ne pouvez pas décider. Fournissez éventuellement les deux versions, peut-être une qui signale une erreur et une encapsuleuse qui convertit l'erreur renvoyée en exception.
Fonction "Application" (spécifique pour un blob de code d'application, peut être réutilisée, mais est limitée par le style de gestion des erreurs des applications, etc.): Ici, elle devrait souvent être assez claire. Si le (s) chemin (s) de code appelant les fonctions gèrent les exceptions de manière saine et utile, utilisez les exceptions pour signaler toute erreur (mais voir ci-dessous) . Si le code d'application est plus facile à lire et à écrire pour un style de retour d'erreur, utilisez-le par tous les moyens.
Bien sûr, il y aura des endroits entre les deux - utilisez simplement ce dont vous avez besoin et souvenez-vous de YAGNI.
Enfin, je pense que je devrais revenir à la déclaration FAQ,
Je souscris à cela pour toutes les erreurs qui indiquent clairement que quelque chose est gravement gâché ou que le code appelant ne savait clairement pas ce qu'il faisait.
Mais lorsque cela est approprié est souvent très spécifique à l'application, donc voir ci-dessus domaine de bibliothèque vs domaine d'application.
Cela revient à la question de savoir si et comment valider les conditions préalables à l' appel , mais je n'entrerai pas dans les détails, répondez déjà trop longtemps :-)
la source