web-dev-qa-db-fra.com

Conception de classe d'exception C ++

Qu'est-ce qu'une bonne conception pour un ensemble de classes d'exception?

Je vois toutes sortes de choses sur ce que les classes d'exception doivent et ne doivent pas faire, mais pas une conception simple qui est facile à utiliser et à étendre et qui fait ces choses.

  1. Les classes d'exceptions ne devraient pas lever d'exceptions, car cela pourrait conduire directement à la fin du processus sans aucune possibilité de consigner l'erreur, etc.
  2. Il doit être possible d'obtenir une chaîne conviviale, de préférence localisée dans leur langue, afin qu'il y ait quelque chose à leur dire avant que l'application ne se termine elle-même si elle ne peut pas se remettre d'une erreur.
  3. Il doit être possible d'ajouter des informations au fur et à mesure que la pile se déroule, par exemple, si un analyseur XML ne parvient pas à analyser un flux d'entrée, pour pouvoir ajouter que la source provient d'un fichier, ou sur le réseau, etc.
  4. Les gestionnaires d'exceptions doivent accéder facilement aux informations dont ils ont besoin pour gérer l'exception.
  5. Écrivez les informations d'exception formatées dans un fichier journal (en anglais, donc pas de traduction ici).

Obtenir 1 et 4 pour travailler ensemble est le plus gros problème que j'ai, car toutes les méthodes de formatage et de sortie de fichier peuvent potentiellement échouer.

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

Quoi qu'il en soit, je pensais que je publierais également la classe d'exception que je pense utiliser.

class Exception : public std::exception
{
    public:
        // Enum for each exception type, which can also be used
        // to determine the exception class, useful for logging
        // or other localisation methods for generating a
        // message of some sort.
        enum ExceptionType
        {
            // Shouldn't ever be thrown
            UNKNOWN_EXCEPTION = 0,

            // The same as above, but it has a string that
            // may provide some information
            UNKNOWN_EXCEPTION_STR,

            // For example, 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é car toutes les chaînes sont gérées en copiant vers un tampon interne de taille fixe (tronqué si nécessaire, mais toujours terminé par null).

Bien que cela ne traite pas du point 3, je pense cependant que ce point est très probablement d'une utilisation limitée dans le monde réel, et pourrait très probablement être résolu en lançant une nouvelle exception si nécessaire.

42
Fire Lancer

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

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

S'il s'agit d'une bibliothèque ou d'un module particulier, vous souhaiterez peut-être une base spécifique à votre module (toujours dérivée de l'une des classes d'exception standard). Les appelants peuvent décider d'attraper quoi que ce soit de votre module de cette façon.

Je ne ferais pas trop de classes d'exception. Vous pouvez regrouper 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'un autre côté, vous voulez des classes uniques pour les erreurs que vous prévoyez de gérer. Si vous créez un analyseur, vous pouvez avoir une seule exception syntax_error avec des membres qui décrivent les détails du problème plutôt qu'un tas de spécialité pour différents types d'erreurs de syntaxe.

Les chaînes des exceptions sont là pour le débogage. Vous ne devez 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 dans 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 chances de construire ou de copier l'exception pour déclencher une autre exception. Par exemple, si vous devez stocker un nom de fichier dans l'exception, vous souhaiterez peut-être un tableau de caractères simples de longueur fixe, plutôt qu'une chaîne std ::. Les implémentations typiques de std :: exception allouent dynamiquement la chaîne de motif à l'aide de malloc. Si le malloc échoue, ils sacrifieront la chaîne de raison plutôt que de lancer une exception imbriquée ou un plantage.

Les exceptions en C++ devraient être pour des conditions "exceptionnelles". Ainsi, les exemples d'analyse peuvent ne pas être bons. Une erreur de syntaxe rencontrée lors de l'analyse d'un fichier n'est peut-être pas assez spéciale pour justifier d'être géré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 gérée. Ainsi, la plupart des échecs d'allocation de mémoire sont exceptionnels, mais une mauvaise entrée d'un utilisateur ne l'est probablement pas.

24
Adrian McCarthy

Utilisez l'héritage virtuel . Cette idée est due à Andrew Koenig. L'utilisation de l'héritage virtuel de la ou des classes de base de votre exception évite les problèmes d'ambiguïté sur le site de capture au cas où quelqu'un lève une exception dérivée de plusieurs bases ayant une classe de base en commun.

Autres conseils tout aussi utiles sur le site boost

7
jon-hanson


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 être effectuée à un niveau externe lorsque l'application se rend compte qu'elle ne peut pas gérer le problème. La plupart des informations contenues dans une exception représentent trop de détails d'implémentation pour être affichées de toute façon par un utilisateur.
3: Utilisez boost.exception pour cela
5: Non, ne faites pas ça. Voir 2. La décision de se connecter doit toujours être prise sur le site de gestion des erreurs.

N'utilisez qu'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 nécessaire

7
grimner

Pas directement lié à la conception d'une hiérarchie de classes d'exceptions, mais important (et lié à l'utilisation de ces exceptions) est que vous devriez généralement lancer par valeur et intercepter par référence.

Cela évite les problèmes liés à la gestion de la mémoire de l'exception levée (si vous avez lancé des pointeurs) et au potentiel de découpage d'objet (si vous interceptez les exceptions par valeur).

4
Michael Burr

Puisque std::nested_exception et std::throw_with_nested sont devenus disponibles avec C++ 11 , je voudrais indiquer des 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 encombrante, en écrivant simplement un gestionnaire d'exceptions approprié qui renverra les exceptions imbriquées.

À mon avis, la conception des exceptions suggère également de ne pas créer de hiérarchies de classes d'exceptions, mais de créer uniquement une seule classe d'exceptions par bibliothèque (comme déjà indiqué out in ne réponse à cette question ).

3
GPMueller

Une bonne conception n'est pas de créer un ensemble de classes d'exceptions - il suffit d'en créer une par bibliothèque, basée sur std :: exception.

L'ajout d'informations est assez simple:

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

Et les gestionnaires d'exceptions ont les informations dont ils ont besoin car ce sont des gestionnaires d'exceptions - seules les fonctions dans les blocs try peuvent provoquer des exceptions, de sorte que les gestionnaires n'ont qu'à prendre en compte ces erreurs. Et non, vous ne devriez pas vraiment utiliser d'exceptions pour la gestion générale des erreurs.

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 pour le moment.

2
anon