Pourquoi les spécifications d'exception sont-elles mauvaises?

50

De retour à l'école il y a plus de 10 ans, ils vous apprenaient à utiliser des spécificateurs d'exception. Puisque je suis un des programmeurs C de Torvaldish qui évitent obstinément le C ++ sauf à y être contraint, je ne me retrouve que sporadiquement dans C ++, et j'utilise toujours des spécificateurs d'exception, car c'est ce que l'on m'a appris.

Cependant, la majorité des programmeurs C ++ semblent se désintéresser des spécificateurs d'exception. J'ai lu le débat et les arguments de divers gourous du C ++, comme ceux-ci . Autant que je sache, cela se résume à trois choses:

  1. Les spécificateurs d'exception utilisent un système de types incohérent avec le reste du langage ("système de types d'ombre").
  2. Si votre fonction avec un spécificateur d'exception renvoie autre chose que ce que vous avez spécifié, le programme sera arrêté de manière incorrecte et inattendue.
  3. Les spécificateurs d'exception seront supprimés dans la prochaine norme C ++.

Est-ce que je manque quelque chose ici ou sont-ce toutes les raisons?

Mes propres opinions:

En ce qui concerne 1): Et alors? C ++ est probablement le langage de programmation le plus incohérent jamais créé, en termes de syntaxe. Nous avons les macros, les goto / labels, la horde (comportement?) Du comportement indéfini / non spécifié / défini par l'implémentation, les types entiers mal définis, toutes les règles de promotion des types implicites, les mots clés de cas spéciaux tels que ami, auto , inscrivez-vous, explicite ... Et ainsi de suite. Quelqu'un pourrait probablement écrire plusieurs gros livres de toutes les bizarreries en C / C ++. Alors, pourquoi les gens réagissent-ils contre cette incohérence particulière, qui constitue un défaut mineur par rapport à de nombreuses autres caractéristiques beaucoup plus dangereuses de la langue?

Concernant 2): N'est-ce pas ma propre responsabilité? Il y a tellement d'autres façons de rédiger un bogue fatal en C ++, pourquoi ce cas particulier est-il pire? Au lieu d'écrire throw(int)puis de lancer Crash_t, je peux aussi bien affirmer que ma fonction renvoie un pointeur sur int, puis créer un typecast sauvage et explicite et renvoyer un pointeur sur un Crash_t. L’esprit du C / C ++ a toujours été de laisser la plupart des responsabilités au programmeur.

Qu'en est-il des avantages alors? Le plus évident est que si votre fonction essaie de jeter explicitement un type différent de celui que vous avez spécifié, le compilateur vous donnera une erreur. Je crois que la norme est claire à ce sujet (?). Les bogues ne se produiront que lorsque votre fonction appellera d'autres fonctions qui jetteront le mauvais type.

Venant d’un monde de programmes C embarqués déterministes, je préférerais certainement savoir exactement quelle fonction me lancera. Si quelque chose dans le langage le supporte, pourquoi ne pas l'utiliser? Les alternatives semblent être:

void func() throw(Egg_t);

et

void func(); // This function throws an Egg_t

Je pense qu'il y a de grandes chances pour que l'appelant ignore / oublie de mettre en œuvre le test de capture dans le second cas, moins dans le premier cas.

Si je comprends bien, si l’une ou l’autre de ces deux formes décide de lever soudainement un autre type d’exception, le programme se bloquera. Dans le premier cas, parce qu'il n'est pas autorisé de lancer une autre exception, dans le second, parce que personne ne s'attendait à ce qu'il lance un SpanishInquisition_t et que, par conséquent, cette expression ne soit pas interceptée comme elle aurait dû l'être.

Dans ce dernier cas, avoir une capture de dernier recours (...) au plus haut niveau du programme ne semble pas vraiment mieux qu'un crash de programme: "Hé, quelque part dans votre programme, quelque chose a lancé une exception étrange et non gérée . ". Vous ne pouvez pas récupérer le programme une fois que vous êtes si éloigné de l'exception, la seule chose à faire est de quitter le programme.

Et du point de vue de l'utilisateur, ils se moquaient bien de recevoir une boîte de message malveillante de l'OS disant "Programme terminé. Blablabla à l'adresse 0x12345" ou une boîte de message maléfique de votre programme disant "Exception non gérée: myclass. func.something ". Le bug est toujours là.


Avec la prochaine norme C ++, je n'aurai d'autre choix que d'abandonner les spécificateurs d'exception. Mais je préférerais entendre des arguments solides sur la raison pour laquelle ils sont mauvais, plutôt que "Sa Sainteté l'a dit et c'est ainsi". Peut-être y at-il plus d’arguments contre eux que ceux que j’ai énumérés ou peut-être plus qu’ils ne le pensent?


la source
30
Je suis tenté de réduire cela en tant que "fanfaron déguisé en question". Vous posez trois questions valides sur E.specs, mais pourquoi nous embêtez-vous avec vos divagations C ++ - est-ce-que-si-énervant-et-je-aime-C-meilleur?
Martin Ba
10
@Martin: Parce que je tiens à souligner que je suis à la fois partial et que je ne connais pas tous les détails, mais aussi que je considère le langage avec des yeux naïfs et / ou pas encore ruinés. Mais aussi que C et C ++ sont déjà des langages incroyablement défectueux, donc un défaut plus ou moins importe peu. Le message était en réalité bien pire avant que je ne le rédige :) Les arguments contre les spécificateurs d'exception sont également assez subjectifs et il est donc difficile de les discuter sans devenir subjectif vous-même.
SpanishInquisition_t! Hilarant! Personnellement, j’ai été impressionné par l’utilisation du pointeur de cadre de pile pour émettre des exceptions et semble pouvoir rendre le code beaucoup plus propre. Cependant, je n'ai jamais écrit de code avec des exceptions. Appelez-moi à l'ancienne, mais les valeurs de retour fonctionnent très bien pour moi.
Shahbaz
@Shahbaz Comme on peut le lire entre les lignes, je suis aussi un peu démodé, mais je ne me suis jamais encore demandé si les exceptions en elles-mêmes étaient bonnes ou mauvaises.
2
Le passé du lancer est jeté , pas jeté . C'est un verbe fort.
TRiG

Réponses:

48

Les spécifications d'exception sont mauvaises parce qu'elles sont faiblement appliquées, et donc ne font pas grand-chose, mais elles sont également mauvaises parce qu'elles forcent l'exécution à rechercher des exceptions inattendues afin qu'elles puissent terminer () au lieu d'appeler UB. , cela peut entraîner une perte de performance significative.

En résumé, les spécifications d'exception ne sont pas suffisamment imposées dans le langage pour rendre le code plus sûr, et les implémenter de la manière spécifiée a été une lourde perte de performances.

DeadMG
la source
2
Mais la vérification de l'exécution n'est pas la faute de la spécification d'exception en tant que telle, mais de toute partie de la norme qui stipule qu'il doit y avoir une terminaison si le type incorrect est émis. Ne serait-il pas plus judicieux de supprimer simplement l'exigence menant à cette vérification dynamique et de tout laisser évaluer statiquement par le compilateur? On dirait que RTTI est le vrai coupable ici, ou ...?
7
@Lundin: il n'est pas possible de déterminer de manière statique toutes les exceptions pouvant être émises par une fonction en C ++, car le langage autorise des modifications arbitraires et non déterministes du flux de contrôle à l'exécution (par exemple via les pointeurs de fonction, les méthodes virtuelles et le même). L'implémentation de la vérification statique des exceptions au moment de la compilation, comme Java, nécessiterait des modifications fondamentales du langage et la désactivation de nombreuses fonctions de compatibilité C.
3
@OrbWeaver: Je pense que les vérifications statiques sont tout à fait possibles - la spécification d'exception ferait partie de la spécification d'une fonction (comme la valeur de retour), et les pointeurs de fonction, les remplacements virtuels, etc. devraient avoir des spécifications compatibles. L'objection principale serait que c'est une fonctionnalité tout-ou-rien - les fonctions avec des spécifications d'exception statique ne pourraient pas appeler des fonctions héritées. (Appeler les fonctions C serait bien, car ils ne peuvent rien jeter).
Mike Seymour
2
@Lundin: Les spécifications d'exception n'ont pas été supprimées, elles sont simplement obsolètes. Le code existant qui les utilise sera toujours valide.
Mike Seymour
1
@Lundin: Les anciennes versions de C ++ avec des spécifications d'exception sont parfaitement compatibles. Ils ne manqueront ni à la compilation ni à l’exécution inattendue si le code était valide auparavant.
DeadMG
18

Une des raisons pour lesquelles personne ne les utilise est que votre principale hypothèse est fausse:

"L’avantage le plus évident est que si votre fonction essaie de jeter explicitement un type différent de celui que vous avez spécifié, le compilateur vous donnera une erreur."

struct foo {};
struct bar {};

struct test
{
    void baz() throw(foo)
    {
        throw bar();
    }
};

int main()
{
    test x;
    try { x.baz(); } catch(bar &b) {}
}

Ce programme compile sans erreurs ni avertissements .

De plus, même si l'exception aurait été interceptée , le programme se termine toujours.


Edit: pour répondre à un point de votre question, catch (...) (ou mieux, catch (std :: exeption &) ou une autre classe de base, puis catch (...)) est toujours utile, même si vous ne l'utilisez pas. sais exactement ce qui s'est mal passé.

Considérez que votre utilisateur a appuyé sur un bouton de menu pour "enregistrer". Le gestionnaire du bouton de menu invite l'application à enregistrer. Cela pourrait échouer pour une multitude de raisons: le fichier se trouvait sur une ressource réseau qui avait disparu, il y avait un fichier en lecture seule et il ne pouvait pas être sauvegardé, etc. il ne se soucie que de son succès ou de son échec. Si cela réussit, tout va bien. Si cela échoue, il peut en informer l'utilisateur. La nature exacte de l'exception n'est pas pertinente. De plus, si vous écrivez un code correct et exempt d'exceptions, cela signifie qu'une telle erreur peut se propager à travers votre système sans la détruire - même pour un code qui ne s'en soucie pas. Cela facilite l'extension du système. Par exemple, vous enregistrez maintenant via une connexion à une base de données.

Kaz Dragon
la source
4
@Lundin il a compilé sans avertissements. Et non, vous ne pouvez pas. Pas de manière fiable. Vous pouvez appeler via les pointeurs de fonction ou il peut s'agir d'une fonction membre virtuelle et la classe dérivée lève dérive_exception (que la classe de base ne peut pas connaître).
Kaz Dragon
Mauvais chance. Embarcadero / Borland donne un avertissement pour ceci: "une exception de lancement viole le spécificateur d'exception". Apparemment, GCC ne le fait pas. Il n'y a aucune raison pour qu'ils ne puissent pas implémenter un avertissement. De même, un outil d'analyse statique pourrait facilement trouver ce bogue.
14
@Lundin: S'il vous plaît ne commencez pas à discuter et à répéter de fausses informations. Votre question était déjà une sorte de diatribe, et affirmer de fausses choses n'aide pas. Un outil d'analyse statique ne peut pas trouver si cela se produit; le faire impliquerait la résolution du problème de l’arrêt. En outre, il faudrait analyser l'ensemble du programme et très souvent, la source complète n'est pas disponible.
David Thornley
1
@Lundin, le même outil d'analyse statique peut vous indiquer les exceptions renvoyées qui quittent votre pile. Par conséquent, l'utilisation de spécifications d'exception ne vous permet toujours rien d'acheter, à l'exception d'un faux négatif négatif susceptible de détruire votre programme là où vous auriez pu gérer le cas d'erreur. un moyen beaucoup plus agréable (comme annoncer l’échec et continuer à fonctionner: voir la seconde partie de ma réponse pour un exemple).
Kaz Dragon
1
@ Lundin Une bonne question. La réponse est que, en général, vous ne savez pas si les fonctions que vous appelez généreront des exceptions; Où trouver ces réponses est une question à laquelle j'ai répondu dans stackoverflow.com/questions/4025667/… . Les gestionnaires d’événements, en particulier, forment des limites naturelles de modules. Autoriser les exceptions à s'échapper dans un système de fenêtre / événement générique (par exemple) va être puni. Le gestionnaire devrait tous les attraper si le programme doit y survivre.
Kaz Dragon
17

Cet entretien avec Anders Hejlsberg est assez célèbre. Dans ce document, il explique pourquoi l'équipe de conception de C # a tout d'abord ignoré les exceptions vérifiées. En résumé, il y a deux raisons principales: la version et l'évolutivité.

Je sais que l'OP se concentre sur le C ++ alors que Hejlsberg discute de C #, mais les arguments avancés par Hejlsberg sont parfaitement applicables au C ++.

CesarGon
la source
5
"Les points soulevés par Hejlsberg sont parfaitement applicables au C ++ aussi" .. Faux! Les exceptions cochées mises en œuvre dans Java obligent l'appelant à les gérer (et / ou l'appelant, etc.). Caractéristiques d'exception en C ++ (tout assez faible et inutile) ne sont applicables à une seule fonction « appel-frontière » et ne pas « Propager la pile d'appels » comme exceptions vérifiées font.
Martin Ba
3
@Martin: Je suis d'accord avec vous sur le comportement des spécifications d'exception en C ++. Mais ma phrase que vous citez "les remarques de Hejlsberg sont parfaitement applicables au C ++ aussi" fait référence aux remarques de Hejlsberg, plutôt qu'aux spécifications C ++. En d'autres termes, je parle ici de la version et de l'évolutivité du programme, plutôt que de la propagation des exceptions.
CesarGon
3
Je suis en désaccord assez tôt avec Hejlsberg: "Ce qui me préoccupe au sujet des exceptions vérifiées, ce sont les menottes qu’ils placent sur les programmeurs. Vous voyez des programmeurs choisir de nouvelles API qui contiennent toutes ces clauses" Throws ", puis vous voyez à quel point leur code est compliqué, et vous vous rendez compte que les exceptions cochées ne les aident pas. C'est en quelque sorte ces concepteurs d'API dictatoriaux qui vous disent comment gérer vos exceptions. " --- Les exceptions étant vérifiées ou non, vous devez toujours gérer les exceptions émises par l'API que vous appelez. Les exceptions cochées le rendent simplement explicite.
quant_dev
5
@quant_dev: Non, vous n'avez pas à gérer les exceptions émises par l'API que vous appelez. Une option parfaitement valable est de ne pas gérer les exceptions et de les laisser bouger la pile d'appels pour que votre appelant les traite.
CesarGon
4
@CesarGon Ne pas gérer les exceptions, c'est aussi choisir comment les gérer.
quant_dev