web-dev-qa-db-fra.com

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

De retour à l'école il y a plus de 10 ans, ils vous apprenaient à utiliser des spécificateurs d'exceptions. Étant donné que mon expérience est l'un des programmeurs C Torvaldish qui évitent obstinément le C++ à moins d'y être forcé, je ne me retrouve qu'en C++ sporadiquement, et quand je le fais, j'utilise toujours des spécificateurs d'exception puisque c'est ce que j'ai appris.

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

  1. Les spécificateurs d'exceptions utilisent un système de types qui n'est pas cohérent avec le reste du langage ("système de type shadow").
  2. Si votre fonction avec un spécificateur d'exception renvoie autre chose que ce que vous avez spécifié, le programme se terminera de manière incorrecte et inattendue.
  3. Les spécificateurs d'exceptions seront supprimés dans la prochaine norme C++.

Suis-je en train de manquer quelque chose ici ou sont-ce toutes les raisons?

Mes propres opinions:

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

Concernant 2): N'est-ce pas ma propre responsabilité? Il y a tellement d'autres façons d'écrire 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 prétendre que ma fonction retourne un pointeur sur int, puis crée un transtypage explicite et sauvage et renvoie un pointeur sur Crash_t. L'esprit de C/C++ a toujours été de laisser l'essentiel de la responsabilité au programmeur.

Et les avantages alors? Le plus évident est que si votre fonction essaie de lancer explicitement tout type autre que celui que vous avez spécifié, le compilateur vous donnera une erreur. Je pense que la norme est claire à ce sujet (?). Les bogues ne se produisent que lorsque votre fonction appelle d'autres fonctions qui, à leur tour, lancent le mauvais type.

Venant d'un monde de programmes C intégrés et déterministes, je préférerais très certainement savoir exactement ce qu'une fonction va me lancer. S'il y a quelque chose dans la langue qui le soutient, 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 une grande chance que l'appelant ignore/oublie d'implémenter le try-catch dans le deuxième cas, moins dans le premier cas.

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

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

Et du point de vue de l'utilisateur, ils s'en moquent s'ils obtiennent une boîte de message malveillante du système d'exploitation indiquant "Programme terminé. Blablabla à l'adresse 0x12345" ou une boîte de message malveillante de votre programme disant "Exception non gérée: ma classe. func.something ". Le bug est toujours là.


Avec la prochaine norme C++, je n'aurai pas d'autre option que d'abandonner les spécificateurs d'exceptions. Mais je préfère entendre un argument solide pour expliquer pourquoi ils sont mauvais, plutôt que "Sa Sainteté l'a déclaré et il en est ainsi". Peut-être y a-t-il plus d'arguments contre eux que ceux que j'ai énumérés, ou peut-être y en a-t-il plus que je ne le pense?

51
user29079

Les spécifications d'exception sont mauvaises car elles sont faiblement appliquées, et par conséquent n'accomplissent pas grand-chose, et elles sont également mauvaises car elles forcent le run-time à rechercher des exceptions inattendues afin qu'elles puissent terminer (), au lieu d'invoquer UB , cela peut gaspiller une quantité importante de performances.

Donc, en résumé, les spécifications d'exception ne sont pas suffisamment appliquées dans le langage pour réellement rendre le code plus sûr, et leur mise en œuvre comme spécifié était un gros drain de performance.

50
DeadMG

Personne ne les utilise parce que votre hypothèse principale est fausse:

"[L'avantage] le plus évident est que si votre fonction essaie de lancer explicitement tout type autre que 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 se compile avec pas d'erreurs ni d'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 autre classe de base, et puis catch (...)) est toujours utile même si vous ne savez pas exactement ce qui a mal tourné.

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 pouvait é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. Mais le gestionnaire ne se soucie pas vraiment de la raison pour laquelle quelque chose a échoué; il se soucie seulement d'avoir réussi ou échoué. S'il réussit, tout va bien. S'il échoue, il peut en informer l'utilisateur. La nature exacte de l'exception n'est pas pertinente. De plus, si vous écrivez un code approprié et protégé contre les exceptions, cela signifie qu'une telle erreur peut se propager à travers votre système sans le faire tomber - même pour le code qui ne se soucie pas de l'erreur. Cela facilite l'extension du système. Par exemple, vous enregistrez maintenant via une connexion à la base de données. Allez-vous propager throw (SQLException) dans la pile de fonctions tout le long, ou simplement le classer comme "erreur" et le gérer correctement déjà avec toutes les autres choses qui pourraient mal tourner?

19
Kaz Dragon

Cette interview avec Anders Hejlsberg est assez célèbre. Dans ce document, il explique pourquoi l'équipe de conception C # a rejeté les exceptions vérifiées en premier lieu. En bref, il y a deux raisons principales: la versionnabilité et l'évolutivité.

Je sais que l'OP se concentre sur le C++ tandis que Hejlsberg discute du C #, mais les points soulevés par Hejlsberg sont parfaitement applicables au C++ également.

17
CesarGon