Progettazione di classi di eccezione


9

Sto codificando una piccola libreria e ho dei problemi con la progettazione della gestione delle eccezioni. Devo dire che sono (ancora) confuso da questa caratteristica del linguaggio C ++ e ho cercato di leggere il più possibile sull'argomento per capire cosa avrei dovuto fare per lavorare correttamente con le classi di eccezione.

Ho deciso di utilizzare un system_errortipo di approccio prendendo ispirazione dall'implementazione STL della future_errorclasse.

Ho un elenco che contiene i codici di errore:

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

e una singola classe di eccezione (supportata da un error_categorytipo di strutture e tutto il resto necessario dal system_errormodello):

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

Ho solo un piccolo numero di situazioni in cui lancio eccezioni illustrate dall'enumerazione del codice di errore.

Quanto sopra non ha funzionato bene con uno dei miei recensori. Era dell'opinione che avrei dovuto creare una gerarchia di classi di eccezioni con una classe di base derivata std::runtime_errorperché avere il codice di errore incorporato nella condizione mescola cose - eccezioni e codici di errore - e sarebbe più noioso occuparsi di un punto di gestione; la gerarchia delle eccezioni consentirebbe anche una facile personalizzazione del messaggio di errore.

Uno dei miei argomenti era che volevo mantenerlo semplice, che la mia libreria non aveva bisogno di generare più tipi di eccezioni e che la personalizzazione è anche facile in questo caso in quanto viene gestita automaticamente - error_codeha un error_categoryassociato con esso che traduce il codice al messaggio di errore corretto.

Devo dire che non ho difeso bene la mia scelta, a testimonianza del fatto che ho ancora dei malintesi riguardo alle eccezioni C ++.

Vorrei sapere se il mio design ha un senso. Quali sarebbero i vantaggi dell'altro metodo rispetto a quello che ho scelto in quanto devo ammettere che non riesco a vederlo anche io? Cosa potrei fare per migliorare?


2
Tendo a concordare in linea di principio con il tuo recensore (mescolare codici di errore ed eccezioni non è poi così utile). Ma a meno che tu non abbia una grande biblioteca con una grande gerarchia non è utile. Un'eccezione di base che contiene una stringa di messaggio, ha eccezioni separate solo se il catcher dell'eccezione può potenzialmente utilizzare l'unicità dell'eccezione per risolvere il problema.
Martin York,

Risposte:


9

Penso che il tuo collega avesse ragione: stai progettando i tuoi casi di eccezione in base a quanto sia semplice implementare all'interno della gerarchia, non in base alle esigenze di gestione delle eccezioni del codice client.

Con un tipo di eccezione e un'enumerazione per la condizione di errore (soluzione), se il codice client deve gestire singoli casi di errore (ad esempio my_errc::error_x), devono scrivere codice in questo modo:

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

Con più tipi di eccezione (con una base comune per l'intera gerarchia), puoi scrivere:

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

dove le classi di eccezione si presentano così:

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

Quando si scrive una libreria, l'attenzione dovrebbe essere sulla sua facilità d'uso, non (necessariamente) sull'implementazione interna.

Dovresti compromettere la facilità d'uso (come apparirà il codice client) quando lo sforzo di farlo correttamente nella libreria è proibitivo.


0

Sono d'accordo con i tuoi recensori e con @utnapistim. È possibile utilizzare l' system_errorapproccio quando si implementano elementi multipiattaforma quando alcuni errori richiedono una gestione speciale. Ma anche in questo caso, non è una buona soluzione, ma una soluzione meno cattiva.

Un'altra cosa. Quando si crea una gerarchia di eccezioni, non renderla molto profonda. Creare solo quelle classi di eccezioni che possono essere elaborate dai client. Nella maggior parte dei casi uso solo std::runtime_errore std::logic_error. Lancio std::runtime_errorquando qualcosa va storto e non posso fare nulla (l'utente espelle il dispositivo dal computer, ha dimenticato l'applicazione ancora in esecuzione), e std::logic_errorquando la logica del programma si rompe (l'utente tenta di eliminare il record dal database che non esiste, ma prima di eliminare l'operazione ha può controllarlo, quindi ottiene un errore logico).

E come sviluppatore di librerie, pensa alle esigenze dei tuoi utenti. Prova a usarlo da solo e pensa, se ti conforta. Quindi puoi spiegare la tua posizione ai tuoi revisori con esempi di codice.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.