Projektowanie klas wyjątków

9

Koduję małą bibliotekę i mam problemy z zaprojektowaniem obsługi wyjątków. Muszę powiedzieć, że jestem (nadal) zdezorientowany tą funkcją języka C ++ i starałem się przeczytać jak najwięcej na ten temat, aby zrozumieć, co powinienem zrobić, aby poprawnie pracować z klasami wyjątków.

Zdecydowałem się zastosować takie system_errorpodejście, które czerpie inspirację z wdrożenia future_errorklasy STL .

Mam wyliczenie zawierające kody błędów:

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

oraz pojedyncza klasa wyjątków (wspierana przez error_categoryrodzaj struktur i wszystko inne, czego potrzebuje system_errormodel):

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

Mam tylko niewielką liczbę sytuacji, w których zgłaszam wyjątki zilustrowane przez wyliczenie kodu błędu.

Powyższe nie pasowało do jednego z moich recenzentów. Był zdania, że ​​powinienem był stworzyć hierarchię klas wyjątków z klasą pochodną, std::runtime_errorponieważ osadzenie kodu błędu w tym warunku łączy różne rzeczy - wyjątki i kody błędów - i byłoby bardziej żmudne zajmowanie się kwestią obsługi; hierarchia wyjątków pozwoliłaby również na łatwą personalizację komunikatu o błędzie.

Jednym z moich argumentów było to, że chciałem uprościć sprawę, że moja biblioteka nie musiała zgłaszać wielu rodzajów wyjątków i że dostosowanie jest również łatwe w tym przypadku, ponieważ jest obsługiwane automatycznie - error_codema error_categorypowiązaną z nią translację kod do właściwego komunikatu o błędzie.

Muszę powiedzieć, że nie broniłem dobrze mojego wyboru, co świadczy o tym, że nadal mam pewne nieporozumienia dotyczące wyjątków w C ++.

Chciałbym wiedzieć, czy mój projekt ma sens. Jakie byłyby zalety tej drugiej metody w porównaniu z tą, którą wybrałem, ponieważ muszę przyznać, że również tego nie widzę? Co mogę zrobić, aby poprawić?

celavek
źródło
2
Zazwyczaj zgadzam się z twoim recenzentem (mieszanie kodów błędów i wyjątków nie jest tak przydatne). Ale jeśli nie masz ogromnej biblioteki o dużej hierarchii, również nie jest to przydatne. Wyjątek podstawowy, który zawiera ciąg komunikatu, ma osobne wyjątki tylko wtedy, gdy łapacz wyjątku może potencjalnie wykorzystać unikalność wyjątku do rozwiązania problemu.
Martin York,

Odpowiedzi:

9

Myślę, że twój kolega miał rację: projektujesz przypadki wyjątków w oparciu o to, jak proste jest wdrożenie w hierarchii, a nie w oparciu o potrzeby obsługi wyjątków kodu klienta.

Z jednym typem wyjątku i wyliczeniem warunku błędu (twoje rozwiązanie), jeśli kod klienta musi obsługiwać przypadki pojedynczego błędu (na przykład my_errc::error_x), musi napisać kod w następujący sposób:

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

Przy wielu typach wyjątków (mających wspólną podstawę dla całej hierarchii) możesz napisać:

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

gdzie klasy wyjątków wyglądają tak:

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

Podczas pisania biblioteki należy skupić się na łatwości użytkowania, a nie (koniecznie) łatwości wewnętrznej implementacji.

Powinieneś ograniczać łatwość użycia (jak będzie wyglądał kod klienta) tylko wtedy, gdy wysiłek zrobienia go bezpośrednio w bibliotece jest zaporowy.

utnapistim
źródło
0

Zgadzam się z twoimi recenzentami i @utnapistim. Możesz zastosować system_errorpodejście, gdy wdrażasz rzeczy między platformami, gdy niektóre błędy wymagają specjalnej obsługi. Ale nawet w tym przypadku nie jest to dobre rozwiązanie, ale mniej złe rozwiązanie.

Jeszcze jedna rzecz. Podczas tworzenia hierarchii wyjątków nie rób jej zbyt głębokiej. Utwórz tylko te klasy wyjątków, które mogą być przetwarzane przez klientów. W większości przypadków używam tylko std::runtime_errori std::logic_error. Rzucam, std::runtime_errorgdy coś idzie nie tak i nie mogę nic zrobić (użytkownik wysuwa urządzenie z komputera, zapomniał, że aplikacja nadal działa), a std::logic_errorgdy logika programu jest zepsuta (użytkownik próbuje usunąć rekord z bazy danych, który nie istnieje, ale przed usunięciem operacji on może to sprawdzić, więc dostanie błąd logiczny).

A jako twórca bibliotek, pomyśl o potrzebach użytkowników. Spróbuj użyć go sam i zastanów się, czy to dla ciebie komfort. Następnie możesz wyjaśnić swoje stanowisko recenzentom za pomocą przykładów kodu.


źródło