Ausnahmeklassen entwerfen

9

Ich codiere eine kleine Bibliothek und habe Probleme beim Entwerfen der Ausnahmebehandlung. Ich muss sagen, dass ich (immer noch) durch diese Funktion der C ++ - Sprache verwirrt bin und versucht habe, so viel wie möglich zu diesem Thema zu lesen, um zu verstehen, was ich tun müsste, um mit Ausnahmeklassen richtig zu arbeiten.

Ich entschied mich für einen system_errorAnsatz, der sich von der STL-Implementierung der future_errorKlasse inspirieren ließ .

Ich habe eine Aufzählung mit den Fehlercodes:

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

und eine einzelne Ausnahmeklasse (gesichert durch eine error_categoryArt von Strukturen und alles andere, was das system_errorModell benötigt ):

// 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 { };
    }

Ich habe nur eine kleine Anzahl von Situationen, in denen ich Ausnahmen auslöse, die durch die Aufzählung der Fehlercodes veranschaulicht werden.

Das oben Gesagte passte nicht gut zu einem meiner Rezensenten. Er war der Meinung, dass ich eine Hierarchie von Ausnahmeklassen mit einer abgeleiteten Basisklasse std::runtime_errorhätte erstellen sollen, da der in die Bedingung eingebettete Fehlercode Dinge vermischt - Ausnahmen und Fehlercodes - und es mühsamer wäre, sich mit dem Punkt zu befassen der Handhabung; Die Ausnahmehierarchie würde auch eine einfache Anpassung der Fehlermeldung ermöglichen.

Eines meiner Argumente war, dass ich es einfach halten wollte, dass meine Bibliothek nicht mehrere Arten von Ausnahmen auslösen musste und dass die Anpassung auch in diesem Fall einfach ist, da sie automatisch durchgeführt wird - dem error_codeist eine error_categoryzugeordnet, die das übersetzt Code zur richtigen Fehlermeldung.

Ich muss sagen, dass ich meine Wahl nicht gut verteidigt habe, ein Beweis dafür, dass ich immer noch einige Missverständnisse in Bezug auf C ++ - Ausnahmen habe.

Ich würde gerne wissen, ob mein Design Sinn macht. Was wären die Vorteile der anderen Methode gegenüber der von mir gewählten, da ich zugeben muss, dass ich das auch nicht sehe? Was könnte ich tun, um mich zu verbessern?

Celavek
quelle
2
Ich stimme Ihrem Rezensenten im Prinzip zu (das Mischen von Fehlercodes und Ausnahmen ist nicht wirklich nützlich). Aber es sei denn, Sie haben eine riesige Bibliothek mit einer großen Hierarchie, ist auch nicht nützlich. Eine Basisausnahme, die eine Nachrichtenzeichenfolge enthält, hat nur separate Ausnahmen, wenn der Fänger der Ausnahme möglicherweise die Eindeutigkeit der Ausnahme verwenden kann, um das Problem zu beheben.
Martin York

Antworten:

9

Ich denke, Ihr Kollege hatte Recht: Sie entwerfen Ihre Ausnahmefälle basierend auf der einfachen Implementierung innerhalb der Hierarchie und nicht basierend auf den Ausnahmebehandlungsanforderungen des Client-Codes.

Wenn der Clientcode mit einem Ausnahmetyp und einer Aufzählung für die Fehlerbedingung (Ihre Lösung) einzelne Fehlerfälle behandeln muss (z. B. my_errc::error_x), muss er Code wie folgt schreiben:

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
    }
}

Mit mehreren Ausnahmetypen (mit einer gemeinsamen Basis für die gesamte Hierarchie) können Sie schreiben:

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

wo Ausnahmeklassen so aussehen:

// 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 { ... };

Beim Schreiben einer Bibliothek sollte der Schwerpunkt auf der Benutzerfreundlichkeit und nicht (notwendigerweise) auf der Einfachheit der internen Implementierung liegen.

Sie sollten die Benutzerfreundlichkeit (wie Client-Code aussehen wird) nur dann beeinträchtigen, wenn der Aufwand, ihn direkt in der Bibliothek auszuführen, unerschwinglich ist.

utnapistim
quelle
0

Ich stimme Ihren Rezensenten und @utnapistim zu. Sie können den system_errorAnsatz verwenden, wenn Sie plattformübergreifende Dinge implementieren, wenn einige Fehler eine spezielle Behandlung erfordern. Aber auch in diesem Fall ist es keine gute, sondern eine weniger böse Lösung.

Eine Sache noch. Machen Sie beim Erstellen einer Ausnahmehierarchie diese nicht sehr tief. Erstellen Sie nur die Ausnahmeklassen, die von Clients verarbeitet werden können. In den meisten Fällen benutze ich nur std::runtime_errorund std::logic_error. Ich werfe, std::runtime_errorwenn etwas schief geht und ich nichts tun kann (Benutzer wirft Gerät vom Computer aus, er hat vergessen, dass die Anwendung noch läuft), und std::logic_errorwenn die Programmlogik unterbrochen ist (Benutzer versucht, einen Datensatz aus einer nicht vorhandenen Datenbank zu löschen, aber vor dem Löschvorgang er kann es überprüfen, so dass er einen logischen Fehler bekommt).

Denken Sie als Bibliotheksentwickler an die Bedürfnisse Ihrer Benutzer. Versuchen Sie es selbst zu benutzen und überlegen Sie, ob es Ihnen tröstet. Dann können Sie Ihren Prüfern Ihre Position anhand von Codebeispielen erläutern.


quelle