Conception de classe d’exception c ++

Qu’est-ce qu’un bon design pour un ensemble de classes d’exception? Je vois toutes sortes de choses à propos de ce que les classes d’exception devraient et ne devraient pas faire, mais pas une conception simple, facile à utiliser et à développer.

  1. Les classes d’exception ne doivent pas renvoyer d’exceptions, car cela pourrait conduire à la fin du processus sans aucune chance de consigner l’erreur, etc.
  2. Il doit être possible d’obtenir une chaîne conviviale, de préférence localisée dans leur langage, de sorte qu’il y ait quelque chose à leur dire avant que l’application ne se termine si elle ne peut pas récupérer d’une erreur.
  3. Il doit être possible d’append des informations à mesure que la stack se déroule, par exemple si un parsingur XML ne parvient pas à parsingr un stream d’entrée, à pouvoir append que la source provient d’un fichier, ou sur le réseau, etc.
  4. Les gestionnaires d’exceptions ont besoin d’un access facile aux informations dont ils ont besoin pour gérer l’exception
  5. Écrire des informations sur les exceptions mises en forme dans un fichier journal (en anglais, donc pas de traductions ici).

Le plus gros problème que je puisse rencontrer est de faire en sorte que les méthodes 1 et 4 fonctionnent ensemble, car toutes les méthodes de formatage et de sortie de fichier risquent d’échouer.

EDIT: Donc, après avoir examiné les classes d’exception dans plusieurs classes, ainsi que dans la question à laquelle Neil était confronté, il semble pratique courante d’ignorer complètement le point 1 (et donc les recommandations de boost), ce qui semble être une mauvaise idée de moi.

Quoi qu’il en soit, je pensais que id posterait aussi la classe d’exception que je pensais utiliser.

class Exception : public std::exception { public: //enum for each exception type, which can also be used to determin //exception class, useful for logging or other localisation methods //for generating a message of some sort. enum ExceptionType { //shouldnt ever be thrown UNKNOWN_EXCEPTION = 0, //same as above but has a ssortingng that may provide some info UNKNOWN_EXCEPTION_STR, //eg file not found FILE_OPEN_ERROR, //lexical cast type error TYPE_PARSE_ERROR, //NOTE: in many cases functions only check and throw this in debug INVALID_ARG, //an error occured while trying to parse data from a file FILE_PARSE_ERROR, } virtual ExceptionType getExceptionType()const throw() { return UNKNOWN_EXCEPTION; } virtual const char* what()throw(){return "UNKNOWN_EXCEPTION";} }; class FileOpenError : public Exception { public: enum Reason { FILE_NOT_FOUND, LOCKED, DOES_NOT_EXIST, ACCESS_DENIED }; FileOpenError(Reason reason, const char *file, const char *dir)throw(); Reason getReason()const throw(); const char* getFile()const throw(); const char* getDir ()const throw(); private: Reason reason; static const unsigned FILE_LEN = 256; static const unsigned DIR_LEN = 256; char file[FILE_LEN], dir[DIR_LEN]; }; 

Le point 1 est adressé puisque toutes les chaînes sont gérées en les copiant dans un tampon interne de taille fixe (en les tronquant si nécessaire, mais toujours avec une terminaison nulle).

Bien que cela n’aborde pas le point 3, je pense toutefois que ce point est très probablement peu utilisé dans le monde réel, et pourrait très probablement être résolu en lançant une nouvelle exception si nécessaire.

Utilisez une hiérarchie superficielle de classes d’exception. Rendre la hiérarchie trop profonde ajoute plus de complexité que de valeur.

Dérivez vos classes d’exception à partir de std :: exception (ou de l’une des autres exceptions standard comme std :: runtime_error). Cela permet aux gestionnaires d’exceptions génériques au plus haut niveau de gérer les exceptions que vous n’avez pas. Par exemple, il peut y avoir un gestionnaire d’exceptions qui enregistre les erreurs.

Si cela concerne une bibliothèque ou un module particulier, vous pouvez souhaiter une base spécifique à votre module (toujours dérivée d’une des classes d’exception standard). Les appelants peuvent décider d’attraper quelque chose de votre module de cette façon.

Je ne ferais pas trop de classes d’exception. Vous pouvez mettre beaucoup de détails sur l’exception dans la classe, vous n’avez donc pas nécessairement besoin de créer une classe d’exception unique pour chaque type d’erreur. D’autre part, vous voulez des classes uniques pour les erreurs que vous prévoyez de gérer. Si vous faites un parsingur, vous pouvez avoir une seule exception syntax_error avec les membres qui décrivent les détails du problème plutôt qu’un groupe de problèmes spécifiques pour différents types d’erreurs de syntaxe.

Les chaînes dans les exceptions sont là pour le débogage. Vous ne devriez pas les utiliser dans l’interface utilisateur. Vous voulez garder l’interface utilisateur et la logique aussi séparées que possible, pour permettre des choses comme la traduction vers d’autres langues.

Vos classes d’exception peuvent avoir des champs supplémentaires avec des détails sur le problème. Par exemple, une exception syntax_error peut avoir le nom du fichier source, le numéro de ligne, etc. Autant que possible, respectez les types de base pour ces champs afin de réduire les risques de création ou de copie de l’exception pour déclencher une autre exception. Par exemple, si vous devez stocker un nom de fichier dans l’exception, vous pouvez choisir un tableau de caractères simples de longueur fixe, plutôt qu’un std :: ssortingng. Les implémentations typiques de std :: exception allouent dynamicment la chaîne de raison en utilisant malloc. Si malloc échoue, ils sacrifieront la chaîne de raison plutôt que de lancer une exception nestede ou de planter.

Les exceptions en C ++ devraient être pour des conditions “exceptionnelles”. Les exemples d’parsing peuvent donc ne pas être bons. Une erreur de syntaxe rencontrée lors de l’parsing d’un fichier peut ne pas être suffisamment spécifique pour justifier d’être traitée par des exceptions. Je dirais que quelque chose est exceptionnel si le programme ne peut probablement pas continuer à moins que la condition ne soit explicitement traitée. Ainsi, la plupart des défaillances d’allocation de mémoire sont exceptionnelles, mais les erreurs de saisie d’un utilisateur ne le sont probablement pas.

Utilisez l’inheritance virtuel . Cet aperçu est dû à Andrew Koenig. L’utilisation de l’inheritance virtuel de la ou des classes de base de votre exception évite les problèmes d’ambiguïté sur le catch-site au cas où une exception serait dérivée de plusieurs bases ayant une classe de base en commun.

Autres conseils tout aussi utiles sur le site boost

2: Non, vous ne devez pas mélanger l’interface utilisateur (= messages localisés) avec la logique du programme. La communication avec l’utilisateur doit se faire à un niveau externe lorsque l’application réalise qu’elle ne peut pas gérer le problème. La plupart des informations contenues dans une exception constituent un détail d’implémentation trop important pour permettre à un utilisateur de voir.
3: Utiliser boost.exception pour cela
5: Non, ne faites pas ça. Voir 2. La décision de se connecter doit toujours être sur le site de traitement des erreurs.

Ne pas utiliser un seul type d’exception. Utilisez suffisamment de types pour que l’application puisse utiliser un gestionnaire de capture distinct pour chaque type de récupération d’erreur requirejs

Non directement liée à la conception d’une hiérarchie de classes d’exceptions, mais importante (et liée à l’utilisation de ces exceptions) est que vous devez généralement utiliser la valeur et la capture par référence.

Cela évite les problèmes liés à la gestion de la mémoire de l’exception déclenchée (si vous avez lancé des pointeurs) et à la possibilité de découper des objects (si vous détectez des exceptions par valeur).

Une bonne conception ne consiste pas à créer un ensemble de classes d’exception – créez-en une par bibliothèque, basée sur std :: exception.

L’ajout d’informations est assez facile:

 try { ... } catch( const MyEx & ex ) { throw MyEx( ex.what() + " more local info here" ); } 

Et les gestionnaires d’exceptions disposent des informations dont ils ont besoin car ils sont des gestionnaires d’exceptions – seules les fonctions des blocs try peuvent entraîner des exceptions. Par conséquent, les gestionnaires doivent uniquement prendre en compte ces erreurs. Et vous ne devriez pas vraiment utiliser les exceptions pour la gestion des erreurs générales.

Fondamentalement, les exceptions devraient être aussi simples que possible – un peu comme les fichiers journaux, auxquels ils ne devraient pas avoir de connexion directe.

Cela a déjà été demandé, je pense, mais je ne le trouve pas tout de suite.

Depuis que std::nested_exception et std::throw_with_nested sont disponibles avec C ++ 11 , j’aimerais indiquer les réponses sur StackOverflow ici et ici

Ces réponses décrivent comment vous pouvez obtenir une trace sur vos exceptions dans votre code sans avoir besoin d’un débogueur ou d’une journalisation fastidieuse, en écrivant simplement un gestionnaire d’exceptions approprié qui renverra les exceptions nestedes.

La conception des exceptions, à mon avis, suggère également de ne pas créer de hiérarchies de classes d’exceptions, mais uniquement de créer une seule classe d’exceptions par bibliothèque (comme cela a déjà été souligné dans une réponse à cette question ).