Conception de classes d'exception

9

Je code une petite bibliothèque et j'ai des problèmes avec la conception de la gestion des exceptions. Je dois dire que je suis (encore) confus par cette fonctionnalité du langage C ++ et j'ai essayé de lire autant que possible sur le sujet pour comprendre ce que je devrais faire pour travailler correctement avec les classes d'exception.

J'ai décidé d'utiliser un system_errortype d'approche en m'inspirant de l'implémentation STL de la future_errorclasse.

J'ai une énumération contenant les codes d'erreur:

enum class my_errc : int
{
    error_x = 100,
    error_z = 101,
    error_y = 102
};

et une seule classe d'exception (soutenue par un error_categorytype de structures et tout le reste nécessaire au system_errormodèle):

// error category implementation
class my_error_category_impl : public std::error_category
{
    const char* name () const noexcept override
    {
        return "my_lib";
    }

    std::string  message (int ec) const override
    {
        std::string msg;
        switch (my_errc(ec))
        {
        case my_errc::error_x:
            msg = "Failed 1.";
            break;
        case my_errc::error_z:
            msg = "Failed 2.";
            break;
        case my_errc::error_y:
            msg = "Failed 3.";
            break;
        default:
            msg = "unknown.";
        }

        return msg;
    }

    std::error_condition default_error_condition (int ec) const noexcept override
    {
        return std::error_condition(ec, *this);
    }
};

// unique instance of the error category
struct my_category
{
    static const std::error_category& instance () noexcept
    {
        static my_error_category_impl category;
        return category;
    }
};

// overload for error code creation
inline std::error_code make_error_code (my_errc ec) noexcept
{
    return std::error_code(static_cast<int>(ec), my_category::instance());
}

// overload for error condition creation
inline std::error_condition make_error_condition (my_errc ec) noexcept
{
    return std::error_condition(static_cast<int>(ec), my_category::instance());
}

/**
 * Exception type thrown by the lib.
 */
class my_error : public virtual std::runtime_error
{
public:
    explicit my_error (my_errc ec) noexcept :
        std::runtime_error("my_namespace ")
        , internal_code(make_error_code(ec))
    { }

    const char* what () const noexcept override
    {
        return internal_code.message().c_str();
    }

    std::error_code code () const noexcept
    {
        return internal_code;
    }

private:
    std::error_code internal_code;
};

// specialization for error code enumerations
// must be done in the std namespace

    namespace std
    {
    template <>
    struct is_error_code_enum<my_errc> : public true_type { };
    }

Je n'ai qu'un petit nombre de situations dans lesquelles je lève des exceptions illustrées par l'énumération du code d'erreur.

Ce qui précède ne s'est pas bien passé avec l'un de mes critiques. Il était d'avis que j'aurais dû créer une hiérarchie de classes d'exceptions avec une classe de base dérivée de std::runtime_errorparce que le code d'erreur intégré dans la condition mélange les choses - exceptions et codes d'erreur - et il serait plus fastidieux de traiter un point de manutention; la hiérarchie des exceptions permettrait également une personnalisation facile du message d'erreur.

Un de mes arguments était que je voulais rester simple, que ma bibliothèque n'avait pas besoin de lancer plusieurs types d'exceptions et que la personnalisation est également facile dans ce cas car elle est gérée automatiquement - elle error_codea un error_categoryassocié qui traduit le code au message d'erreur approprié.

Je dois dire que je n'ai pas bien défendu mon choix, témoignant du fait que j'ai encore des malentendus concernant les exceptions C ++.

Je voudrais savoir si mon design a du sens. Quels seraient les avantages de l'autre méthode par rapport à celle que j'ai choisie car je dois admettre que je ne vois pas cela aussi? Que pourrais-je faire pour m'améliorer?

celavek
la source
2
J'ai tendance à être d'accord en principe avec votre critique (mélanger les codes d'erreur et les exceptions n'est pas vraiment utile). Mais à moins que vous ayez une énorme bibliothèque ayant une grande hiérarchie n'est également pas utile. Une exception de base qui contient une chaîne de message n'a des exceptions distinctes que si le receveur de l'exception peut potentiellement utiliser l'unicité de l'exception pour résoudre le problème.
Martin York

Réponses:

9

Je pense que votre collègue avait raison: vous concevez vos cas d'exception en fonction de la simplicité de mise en œuvre dans la hiérarchie, et non en fonction des besoins de gestion des exceptions du code client.

Avec un type d'exception et une énumération pour la condition d'erreur (votre solution), si le code client doit gérer des cas d'erreur uniques (par exemple, my_errc::error_x) ils doivent écrire du code comme ceci:

try {
    your_library.exception_thowing_function();
} catch(const my_error& err) {
    switch(err.code()) { // this could also be an if
    case my_errc::error_x:
        // handle error here
        break;
    default:
        throw; // we are not interested in other errors
    }
}

Avec plusieurs types d'exceptions (ayant une base commune pour toute la hiérarchie), vous pouvez écrire:

try {
    your_library.exception_thowing_function();
} catch(const my_error_x& err) {
    // handle error here
}

où les classes d'exception ressemblent à ceci:

// base class for all exceptions in your library
class my_error: public std::runtime_error { ... };

// error x: corresponding to my_errc::error_x condition in your code
class my_error_x: public my_error { ... };

Lors de l'écriture d'une bibliothèque, l'accent doit être mis sur sa facilité d'utilisation, et non (nécessairement) sur la facilité d'implémentation interne.

Vous ne devez compromettre la facilité d'utilisation (à quoi ressemblera le code client) que lorsque l'effort de le faire correctement dans la bibliothèque est prohibitif.

utnapistim
la source
0

Je suis d'accord avec vos critiques et avec @utnapistim. Vous pouvez utiliser l' system_errorapproche lorsque vous implémentez des choses multiplates-formes lorsque certaines erreurs nécessitent une gestion spéciale. Mais même dans ce cas, ce n'est pas une bonne solution, mais une solution moins mauvaise.

Encore une chose. Lorsque vous créez une hiérarchie d'exceptions, ne la rendez pas très profonde. Créez uniquement les classes d'exception qui peuvent être traitées par les clients. Dans la plupart des cas, j'utilise uniquement std::runtime_erroret std::logic_error. Je lance std::runtime_errorquand quelque chose ne va pas et que je ne peux rien faire (l'utilisateur éjecte le périphérique de l'ordinateur, il a oublié que l'application est toujours en cours d'exécution) et std::logic_errorlorsque la logique du programme est cassée (l'utilisateur essaie de supprimer un enregistrement de la base de données qui n'existe pas, mais avant de supprimer l'opération, il peut le vérifier, donc il obtient une erreur logique).

Et en tant que développeur de bibliothèque, pensez aux besoins de vos utilisateurs. Essayez de l'utiliser vous-même et pensez, si cela vous réconforte. Vous pouvez ensuite expliquer votre position à vos réviseurs avec des exemples de code.


la source