Progettazione di classi di eccezioni

8

Sto codificando una piccola libreria e sto avendo qualche problema 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 arrivare a capire cosa avrei dovuto fare per lavorare correttamente con le classi di eccezioni.

Ho deciso di utilizzare un tipo di approccio system_error prendendo ispirazione dall'implementazione STL della classe future_error .

Ho un'enumerazione contenente i codici di errore:

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

e una singola classe di eccezioni (supportata da un tipo% dierror_category di strutture e tutto il resto richiesto dal modello system_error ):

// 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 si adattava bene a uno dei miei revisori. Era dell'opinione che avrei dovuto creare una gerarchia di classi di eccezioni con una classe base derivata da std::runtime_error perché avere il codice di errore incorporato nella condizione mescola cose - eccezioni e codici di errore - e sarebbe più noioso trattare con un punto di manipolazione; la gerarchia delle eccezioni consentirebbe anche una facile personalizzazione del messaggio di errore.

Uno dei miei argomenti era che volevo semplificarlo, che la mia libreria non aveva bisogno di lanciare più tipi di eccezioni e che la personalizzazione è anche facile in questo caso poiché viene gestita automaticamente - il error_code ha un error_category associato ad esso che traduce il codice nel messaggio di errore corretto.

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

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

    
posta celavek 15.04.2015 - 08:12
fonte

2 risposte

9

Penso che il tuo collega abbia ragione: stai progettando i casi di eccezione sulla base della semplicità con cui implementare all'interno della gerarchia, non sulla base delle esigenze di gestione delle eccezioni del codice client.

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

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 eccezioni hanno questo aspetto:

// 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 scrivi una libreria, l'attenzione dovrebbe essere sulla sua facilità d'uso, non sulla (necessaria) facilità dell'implementazione interna.

Dovresti solo compromettere la facilità d'uso (come sarà il codice cliente) quando lo sforzo di farlo direttamente nella libreria è proibitivo.

    
risposta data 15.04.2015 - 10:14
fonte
0

Sono d'accordo con i tuoi revisori e con @utnapistim. È possibile utilizzare l'approccio system_error quando si implementano cose 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 crei una gerarchia di eccezioni, non renderlo molto profondo. Crea solo quelle classi di eccezioni, che possono essere elaborate dai client. Nella maggior parte dei casi, utilizzo solo std::runtime_error e std::logic_error . Getto std::runtime_error quando qualcosa va storto e non posso fare nulla (dispositivo espellere l'utente dal computer, ha dimenticato che l'applicazione è ancora in esecuzione) e std::logic_error quando la logica del programma è interrotta (l'utente prova ad eliminare il record dal database che non esiste , ma prima di cancellare l'operazione può controllarlo, così ottiene l'errore logico.

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

    
risposta data 16.04.2015 - 02:00
fonte

Leggi altre domande sui tag