J'essaie de convaincre mon chef d'équipe d'autoriser l'utilisation d'exceptions en C ++ au lieu de renvoyer un bool isSuccessful
ou une enum avec le code d'erreur. Cependant, je ne peux pas contrer ces critiques.
Considérez cette bibliothèque:
class OpenFileException() : public std::runtime_error {
}
void B();
void C();
/** Does blah and blah. */
void B() {
// The developer of B() either forgot to handle C()'s exception or
// chooses not to handle it and let it go up the stack.
C();
};
/** Does blah blah.
*
* @raise OpenFileException When we failed to open the file. */
void C() {
throw new OpenFileException();
};
Considérons un développeur appelant la
B()
fonction. Il vérifie sa documentation et constate qu'elle ne renvoie aucune exception. Il n'essaie donc pas d'attraper quoi que ce soit. Ce code pourrait planter le programme en production.Considérons un développeur appelant la
C()
fonction. Il ne vérifie pas la documentation, il ne détecte donc aucune exception. L'appel n'est pas sûr et pourrait bloquer le programme en production.
Mais si nous vérifions les erreurs de cette manière:
void old_C(myenum &return_code);
Un compilateur utilisant cette fonction sera averti par le compilateur s'il ne fournit pas cet argument, et il dira "Aha, cela retourne un code d'erreur que je dois vérifier."
Comment puis-je utiliser les exceptions en toute sécurité, de sorte qu'il existe un type de contrat?
la source
Either
/Result
monad pour renvoyer l’erreur de manière sécurisée à laRéponses:
C'est une critique légitime des exceptions. Ils sont souvent moins visibles que la simple gestion des erreurs, telle que le renvoi d'un code. Et il n'y a pas de moyen facile d'appliquer un "contrat". Une partie du but est de vous permettre de laisser les exceptions être capturées à un niveau supérieur (si vous devez capturer toutes les exceptions à tous les niveaux, en quoi est-il différent de renvoyer un code d'erreur, de toute façon?). Et cela signifie que votre code pourrait être appelé par un autre code qui ne le gère pas correctement.
Les exceptions ont des inconvénients; vous devez présenter un argumentaire fondé sur les coûts et les avantages.
J'ai trouvé ces deux articles utiles: La nécessité des exceptions et Tout va mal avec les exceptions. En outre, cet article de blog offre les opinions de nombreux experts sur les exceptions, en mettant l'accent sur le C ++ . Bien que l'opinion des experts semble pencher en faveur des exceptions, elle est loin d'un consensus clair.
Pour convaincre votre chef d’équipe, ce n’est peut-être pas la bonne bataille à choisir. Surtout pas avec le code hérité. Comme indiqué dans le deuxième lien ci-dessus:
Ajouter un peu de code qui utilise des exceptions à un projet qui ne le fait généralement pas ne sera probablement pas une amélioration. Ne pas utiliser d'exceptions dans du code autrement bien écrit est loin d'être un problème catastrophique; cela peut ne pas être un problème du tout, en fonction de l'application et de l'expert que vous demandez. Vous devez choisir vos batailles.
Ce n’est probablement pas un argument pour lequel je voudrais consacrer des efforts, du moins pas avant le démarrage d’un nouveau projet. Et même si vous avez un nouveau projet, est-ce que celui-ci sera utilisé ou utilisé par un code hérité?
la source
Il y a littéralement des livres entiers écrits sur ce sujet, donc toute réponse sera au mieux un résumé. Voici quelques points importants qui, selon moi, méritent d’être soulignés, en fonction de votre question. Ce n'est pas une liste exhaustive.
Les exceptions ne sont PAS destinées à être capturées partout.
Tant qu'il existe un gestionnaire d'exceptions général dans la boucle principale - en fonction du type d'application (serveur Web, service local, utilitaire de ligne de commande ...) - vous disposez généralement de tous les gestionnaires d'exceptions dont vous avez besoin.
Dans mon code, il n'y a que quelques déclarations catch, voire aucune, en dehors de la boucle principale. Et cela semble être l'approche commune du C ++ moderne.
Les exceptions et les codes de retour ne sont pas mutuellement exclusifs.
Vous ne devriez pas en faire une approche du tout ou rien. Les exceptions doivent être utilisées pour des situations exceptionnelles. Des choses comme "Fichier de configuration introuvable", "Disque plein" ou toute autre chose qui ne peut pas être gérée localement.
Les échecs courants, tels que la vérification de la validité d'un nom de fichier fourni par l'utilisateur, ne constituent pas un cas d'utilisation d'exceptions; Utilisez plutôt une valeur de retour dans ces cas.
Comme vous le voyez dans les exemples ci-dessus, "fichier non trouvé" peut être une exception ou un code retour, selon le cas d'utilisation: "fait partie de l'installation" par rapport à "l'utilisateur peut créer une faute de frappe".
Donc, il n'y a pas de règle absolue. Une directive approximative est la suivante: s’il peut être manipulé localement, faites-en une valeur de retour; si vous ne pouvez pas le gérer localement, lancez une exception.
La vérification statique des exceptions n'est pas utile.
Comme les exceptions ne doivent de toute façon pas être gérées localement, les exceptions pouvant être levées n’ont généralement pas d'importance. La seule information utile est de savoir si une exception peut être levée.
Java a une vérification statique, mais cela est généralement considéré comme une expérience manquée et la plupart des langages depuis - notamment le C # - ne disposent pas de ce type de vérification statique. C’est une bonne lecture des raisons pour lesquelles C # ne l’a pas.
Pour ces raisons, C ++ a déconseillé son utilisation
throw(exceptionA, exceptionB)
en faveur denoexcept(true)
. Le paramètre par défaut est qu'une fonction peut lancer, de sorte que les programmeurs doivent s'y attendre, sauf indication contraire explicite de la documentation.L'écriture de code sécurisé d'exception n'a rien à voir avec l'écriture de gestionnaires d'exceptions.
Je dirais plutôt que l'écriture de code sécurisé d'exception consiste à éviter d'écrire des gestionnaires d'exception!
La plupart des meilleures pratiques visent à réduire le nombre de gestionnaires d'exceptions. Écrire du code une fois et l'invoquer automatiquement - par exemple via RAII - entraîne moins de bogues que le copier-coller du même code partout.
la source
IOException
s aussi . La division vérifiée est arbitraire et inutile.Les programmeurs C ++ ne recherchent pas de spécifications d'exception. Ils recherchent des garanties d'exception.
Supposons qu'un morceau de code lève une exception. Quelles hypothèses le programmeur peut-il émettre et qui resteront valables? D'après la manière dont le code est écrit, que garantit-il à la suite d'une exception?
Ou est-il possible qu'un certain morceau de code puisse garantir de ne jamais lancer (c'est-à-dire, rien de moins que le processus du système d'exploitation ne soit terminé)?
Le mot "roll back" apparaît fréquemment dans les discussions sur les exceptions. La possibilité de revenir à un état valide (explicitement documenté) est un exemple de garantie d'exception. S'il n'y a pas de garantie d'exception, un programme doit se terminer sur-le-champ car il n'est même pas garanti que le code qu'il exécutera par la suite fonctionnera comme prévu.
Diverses techniques de programmation C ++ promeuvent des garanties d’exception. RAII (gestion des ressources basée sur l'étendue) fournit un mécanisme pour exécuter le code de nettoyage et garantir que les ressources sont libérées dans les cas normaux et exceptionnels. Faire une copie des données avant d'effectuer des modifications sur les objets permet de restaurer l'état de cet objet si l'opération échoue. Etc.
Les réponses à cette question de StackOverflow donnent un aperçu des grandes longueurs que les programmeurs C ++ doivent comprendre pour comprendre tous les modes de défaillance possibles de leur code et essaient de préserver la validité de l'état du programme malgré les échecs. L'analyse ligne par ligne du code C ++ devient une habitude.
Lors du développement en C ++ (pour une utilisation en production), on ne peut pas se permettre de passer sous silence les détails. En outre, le blob binaire (non-open-source) est le fléau des programmeurs C ++. Si je dois appeler un objet blob binaire et que celui-ci échoue, alors l'ingénierie inverse est ce qu'un programmeur C ++ ferait ensuite.
Référence: http://fr.cppreference.com/w/cpp/language/exceptions#Exception_safety - voir la section Sécurité des exceptions.
C ++ a échoué dans sa tentative d'implémentation de spécifications d'exception. Une analyse ultérieure dans d'autres langues indique que les spécifications d'exception ne sont tout simplement pas pratiques.
Pourquoi c’est une tentative infructueuse: pour l’appliquer de manière stricte, cela doit faire partie du système de types. Mais ça ne l'est pas. Le compilateur ne vérifie pas la spécification des exceptions.
Pourquoi C ++ a choisi cela, et pourquoi les expériences d'autres langages (Java) prouvent que la spécification d'une exception est sans objet: dès que l'on modifie l'implémentation d'une fonction (par exemple, il doit faire appel à une fonction différente qui peut lancer un nouveau type de exception), une application stricte de spécification d’exception signifie que vous devez également mettre à jour cette spécification. Cela se propage - vous pouvez finir par devoir mettre à jour les spécifications d'exception pour des dizaines ou des centaines de fonctions pour un simple changement. La situation se dégrade pour les classes de base abstraites (l’équivalent C ++ des interfaces). Si la spécification d'exception est imposée sur les interfaces, les implémentations d'interfaces ne seront pas autorisées à appeler des fonctions renvoyant différents types d'exceptions.
Référence: http://www.gotw.ca/publications/mill22.htm
À partir de C ++ 17, l'
[[nodiscard]]
attribut peut être utilisé sur les valeurs de retour de fonction (voir: https://stackoverflow.com/questions/39327028/can-ac-function-be-declared-such-that-the-return-value -cannot-be-ignored ).Donc, si je modifie le code et que cela introduit un nouveau type de condition d'échec (un nouveau type d'exception), s'agit-il d'un changement radical? Devrait-il obliger l'appelant à mettre à jour le code ou au moins être averti du changement?
Si vous acceptez les arguments selon lesquels les programmeurs C ++ recherchent des garanties d'exception au lieu de spécifications d'exception, la réponse est que si le nouveau type de condition d'échec ne casse aucune des exceptions, le code promis précédemment ne constitue pas une modification radicale.
la source
std::exception
) et votre propre exception de base seront levées. Mais appliqué à un projet entier, cela signifie simplement que chaque fonction aura cette même spécification: le bruit.catch
est basé sur le type de l'objet exception, alors que dans de nombreux cas, ce qui compte n'est pas tant la cause directe de l'exception que son implication dans l'état du système. Malheureusement, le type d'une exception ne dit généralement pas si le code a provoqué la sortie d'un élément de code de manière perturbante de manière à laisser un objet dans un état partiellement mis à jour (et donc non valide).C'est totalement sécuritaire. Il n'a pas besoin d'attraper les exceptions à chaque endroit, il peut simplement lancer un essai / attraper dans un endroit où il peut réellement faire quelque chose d'utile à ce sujet. Ce n'est dangereux que s'il laisse filer, mais il n'est généralement pas difficile d'empêcher que cela se produise.
la source
C()
et ne protège donc pas contre le code qui suit l'appel ignoré. Si ce code est nécessaire pour garantir que certaines invariances restent vraies, cette garantie disparaît à la première exception sortant deC()
. Il n’existe pas de logiciel parfaitement sûr pour les exceptions, et très certainement pas où des exceptions n’étaient jamais attendues.C()
est une grosse erreur. La valeur par défaut en C ++ est que n'importe quelle fonction peut lancer - il faut un explicitenoexcept(true)
pour indiquer le compilateur autrement. En l'absence de cette promesse, un programmeur doit supposer qu'une fonction peut lancer.C()
fonctions s'interrompront en présence d'exceptions. C’est vraiment très imprudent, c’est condamné à une tonne de problèmes.Si vous construisez un système critique, suivez les conseils de votre chef d'équipe et évitez les exceptions. Il s'agit de la règle AV 208 dans les normes de codage C ++ des véhicules aériens de combat interarmées de Lockheed Martin . D'autre part, les directives MISRA C ++ contiennent des règles très spécifiques concernant le moment où les exceptions peuvent ou ne peuvent pas être utilisées si vous construisez un système logiciel compatible MISRA.
Si vous construisez des systèmes critiques, il est probable que vous utilisiez également des outils d'analyse statique. De nombreux outils d'analyse statiques vous avertiront si vous ne vérifiez pas la valeur de retour d'une méthode, ce qui rend les cas de traitement d'erreur manquant facilement évidents. Autant que je sache, la prise en charge d'outils similaires pour détecter le traitement correct des exceptions n'est pas aussi efficace.
En fin de compte, je dirais que la conception par contrat et la programmation défensive, associées à l'analyse statique, sont plus sûres que les exceptions pour les systèmes logiciels critiques.
la source