Comment puis-je créer et appliquer des contrats pour des exceptions?


33

J'essaie de convaincre mon chef d'équipe d'autoriser l'utilisation d'exceptions en C ++ au lieu de renvoyer un bool isSuccessfulou 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();
};
  1. 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.

  2. 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?


4
@Sjoerd Le principal avantage est qu'un développeur est forcé par le compilateur de fournir une variable à la fonction pour stocker le code de retour; il est informé qu'il peut y avoir une erreur et qu'il devrait s'en occuper. C’est infiniment meilleur que les exceptions, qui n’ont aucune vérification du temps de compilation.
DBedrenko

4
"il devrait s'en occuper" - il n'y a aucune vérification si cela se produit non plus.
Sjoerd

4
@Sjoerd Ce n'est pas nécessaire. Il suffit que le développeur se rende compte qu'une erreur peut se produire et qu'il devrait le vérifier. C'est également à la vue des relecteurs, qu'ils vérifient une erreur ou non.
DBedrenko

5
Vous aurez peut-être plus de chances d’utiliser un Either/ Resultmonad pour renvoyer l’erreur de manière sécurisée à la
composition

5
@gardenhead Parce que trop de développeurs ne l'utilisent pas correctement, vous devez vous retrouver avec un code déplacé et blâmer la fonctionnalité plutôt que par eux-mêmes La fonctionnalité d’exception cochée en Java est une belle chose, mais elle n’a pas été bien prise parce que certaines personnes ne l’ont tout simplement pas.
AxiomaticNexus

Réponses:


47

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:

Les exceptions ne peuvent être propagées par aucun code qui ne soit protégé par une exception. L'utilisation d'exceptions implique donc que tout le code du projet doit être protégé contre les exceptions.

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é?


1
Merci pour les conseils et les liens. C'est un nouveau projet, pas un héritage. Je me demande pourquoi les exceptions sont si répandues alors qu'elles ont un inconvénient si important qui rend leur utilisation dangereuse. Si vous attrapez une exception de niveaux n supérieurs, puis plus tard, vous modifiez le code et le déplacez, aucune vérification statique n'est effectuée pour garantir que l'exception est toujours interceptée et gérée (et demander aux réviseurs de vérifier toutes les fonctions en profondeur est profond. )
DBedrenko

3
@Dee voir les liens ci-dessus pour certains arguments en faveur des exceptions (j'ai ajouté un lien supplémentaire avec une opinion plus experte).

6
Cette réponse est sur place!
Doc Brown

1
Je peux affirmer par expérience qu’essayer d’ajouter des exceptions à une base de code préexistante est une VRAIMEMENT mauvaise idée. J'ai travaillé avec une telle base de code et c'était VRAIMENT, VRAIMENT difficile de travailler avec parce que vous ne saviez jamais ce qui allait exploser dans votre visage. Les exceptions doivent être utilisées UNIQUEMENT dans les projets pour lesquels vous planifiez leur création.
Michael Kohne

26

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 de noexcept(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.


3
" Tant qu'il existe un gestionnaire d'exceptions général ... vous êtes déjà d'accord. " Cela contredit la plupart des sources, soulignant qu'il est très difficile d'écrire du code sécurisé d'exception.

12
@ dan1111 "L'écriture de code sécurisé d'exception" a peu à voir avec l'écriture de gestionnaires d'exceptions. L'écriture de code sécurisé d'exception consiste à utiliser RAII pour encapsuler des ressources, en utilisant "tout d'abord travailler localement, puis en utilisant swap / move", ainsi que d'autres meilleures pratiques. Celles-ci représentent un défi lorsque vous n'y êtes pas habitués. Mais "écrire des gestionnaires d'exception" ne fait pas partie de ces meilleures pratiques.
Sjoerd

4
@AxiomaticNexus À un certain niveau, vous pouvez expliquer toute utilisation infructueuse d'une fonctionnalité en tant que "mauvais développeur". Les meilleures idées sont celles qui réduisent le mauvais usage, car elles facilitent la tâche. Les exceptions vérifiées statiquement ne font pas partie de cette catégorie. Ils créent un travail disproportionné par rapport à leur valeur, souvent dans des endroits où le code écrit n'a tout simplement pas besoin de s'en soucier. Les utilisateurs sont tentés de les réduire au silence de manière à ce que le compilateur cesse de les déranger. Même s'ils sont bien faits, ils génèrent beaucoup de fouillis.
Jpmc26

4
@AxiomaticNexus La raison pour laquelle il est disproportionné est que cela peut facilement se propager à presque toutes les fonctions de votre base de code . Lorsque toutes les fonctions de la moitié de mes classes accèdent à la base de données, chacune d'entre elles n'a pas besoin de déclarer explicitement qu'elle peut déclencher une exception SQLException; pas plus que chaque méthode de contrôleur qui les appelle. Cette quantité de bruit est en réalité une valeur négative, quelle que soit la quantité de travail à effectuer. Sjoerd enfonce le clou: la plupart de vos codes n'ont pas besoin de se soucier des exceptions, car ils ne peuvent rien y faire .
JPMc26

3
@AxiomaticNexus Oui, parce que les meilleurs développeurs sont si parfaits, leur code gérera toujours parfaitement toutes les entrées utilisateur possibles, et vous ne verrez jamais, jamais, ces exceptions dans prod. Et si vous le faites, cela signifie que vous êtes un échec de la vie. Sérieusement, ne me donne pas cette poubelle. Personne n'est parfait, pas même le meilleur. Et votre application devrait se comporter de manière saine même face à ces erreurs, même si "sainement" est "arrête et renvoie une page / un message d'erreur corrects". Et devinez quoi: c’est la même chose que vous faites avec 99% des IOExceptions aussi . La division vérifiée est arbitraire et inutile.
jpmc26

8

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.


"Lorsqu’on modifie l’implémentation d’une fonction (par exemple, il faut appeler une fonction différente qui peut générer un nouveau type d’exception), une application stricte de la spécification des exceptions signifie que vous devez également mettre à jour cette spécification" - Bon comme vous devriez. Quelle serait l'alternative? Ignorer l'exception nouvellement introduite?
AxiomaticNexus

@AxiomaticNexus C'est également couvert par une garantie d'exception. En fait, je soutiens que la spécification ne peut jamais être complète; la garantie est ce que nous recherchons. Souvent, nous comparons le type d’exception dans le but de déterminer quelle garantie a été cassée - afin de deviner quelle garantie était toujours valide. La spécification d'exception répertorie certains des types que vous devez vérifier, mais rappelez-vous que dans tout système pratique, la liste ne peut pas être complète. La plupart du temps, il oblige les gens à prendre le raccourci de "manger" le type d'exception, en l'enveloppant dans une autre exception, ce qui rend la vérification du type d'exception difficile
correct

@AxiomaticNexus Je ne discute ni pour ni contre l'utilisation de l'exception. L'utilisation typique des exceptions encourage l'utilisation d'une classe d'exception de base et de certaines classes d'exceptions dérivées. En attendant, il faut garder à l'esprit que d'autres types d'exceptions sont possibles, par exemple ceux générés à partir de C ++ STL ou d'autres bibliothèques. On pourrait faire valoir que la spécification d'exception pourrait fonctionner dans ce cas: spécifiez que les exceptions standard ( 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.
rwong

Je tiens à souligner qu'en ce qui concerne les exceptions, C ++ et Java / C # sont des langages différents. Premièrement, C ++ n'est pas sûr pour le texte en ce sens qu'il permet l'exécution de code arbitraire ou l'écrasement de données, deuxièmement, C ++ n'imprime pas de trace de pile. Ces différences ont forcé les programmeurs C ++ à faire des choix différents en termes de gestion des erreurs et des exceptions. Cela signifie également que certains projets pourraient légitimement prétendre que le langage C ++ ne leur convient pas. (Par exemple, je considère que le décodage d'image TIFF ne doit pas être autorisé à être mis en œuvre en C ou C ++.)
rwong

1
@rwong: Un gros problème avec la gestion des exceptions dans les langages qui suivent le modèle C ++ est qu'il catchest 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).
Supercat

4

Considérons un développeur appelant la fonction C (). Il ne vérifie pas la documentation, il ne détecte donc aucune exception. L'appel est dangereux

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.


C'est dangereux car il ne s'attend pas à une exception 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 de C(). Il n’existe pas de logiciel parfaitement sûr pour les exceptions, et très certainement pas où des exceptions n’étaient jamais attendues.
cmaster

1
@ cmaster Lui ne pas s'attendre à une exceptionC() est une grosse erreur. La valeur par défaut en C ++ est que n'importe quelle fonction peut lancer - il faut un explicite noexcept(true)pour indiquer le compilateur autrement. En l'absence de cette promesse, un programmeur doit supposer qu'une fonction peut lancer.
Sjoerd

1
@ cmaster C'est sa propre faute stupide et rien à voir avec l'auteur de C (). La sécurité des exceptions est une chose, il devrait en apprendre davantage et la suivre. S'il appelle une fonction dont il ne sait pas qu'il est nul, il devrait très bien gérer ce qui se passe si elle se déclenche. S'il n'a pas besoin de rien, il devrait trouver une implémentation qui soit.
DeadMG

@Sjoerd et DeadMG: Bien que la norme autorise toute fonction à lever une exception, cela ne signifie pas que les projets doivent autoriser des exceptions dans leur code. Si vous bannissez les exceptions de votre code, tout comme le chef d'équipe du PO semble le faire, vous n'avez pas à programmer de manière sûre. Et si vous essayez de convaincre un chef d'équipe d'autoriser des exceptions dans une base de code qui était auparavant exempte d'exception, de nombreuses C()fonctions s'interrompront en présence d'exceptions. C’est vraiment très imprudent, c’est condamné à une tonne de problèmes.
cmaster

@cmaster Il s'agit d'un nouveau projet - voir le premier commentaire sur la réponse acceptée. Donc, il n'y a pas de fonctions existantes qui vont casser, comme il n'y en a pas.
Sjoerd

3

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.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.