Projetando classes de exceção


9

Estou codificando uma pequena biblioteca e estou tendo problemas para projetar o tratamento de exceções. Devo dizer que estou (ainda) confuso com esse recurso da linguagem C ++ e tentei ler o máximo possível sobre o assunto para entender o que eu teria que fazer para trabalhar adequadamente com classes de exceção.

Decidi usar um system_errortipo de abordagem inspirado na implementação do STL da future_errorclasse.

Eu tenho uma enumeração que contém os códigos de erro:

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

e uma única classe de exceção (suportada por um error_categorytipo de estruturas e tudo o mais necessário pelo system_errormodelo):

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

Eu só tenho um pequeno número de situações nas quais eu lanço exceções ilustradas pela enumeração do código de erro.

O exposto acima não se encaixou bem com um dos meus revisores. Ele era da opinião de que eu deveria ter criado uma hierarquia de classes de exceção com uma classe base derivada std::runtime_errorporque ter o código de erro incorporado na condição mistura coisas - exceções e códigos de erro - e seria mais tedioso lidar com esse ponto. de manuseio; a hierarquia de exceções também permitiria fácil personalização da mensagem de erro.

Um dos meus argumentos era que eu queria simplificá-lo, que minha biblioteca não precisava lançar vários tipos de exceções e que a personalização também é fácil nesse caso, pois é tratada automaticamente - ela error_codetem um error_categoryassociado que traduz o código para a mensagem de erro adequada.

Devo dizer que não defendi bem minha escolha, testemunho do fato de que ainda tenho alguns mal-entendidos sobre as exceções de C ++.

Gostaria de saber se meu design faz sentido. Quais seriam as vantagens do outro método sobre o que eu escolhi, pois tenho que admitir que não vejo isso também? O que eu poderia fazer para melhorar?


2
Costumo concordar em princípio com seu revisor (misturar códigos de erro e exceções não é realmente tão útil). Mas, a menos que você tenha uma enorme biblioteca com uma grande hierarquia, também não é útil. Uma exceção básica que contém uma cadeia de mensagens, possui apenas exceções separadas se o coletor da exceção puder usar potencialmente a exclusividade da exceção para corrigir o problema.
Martin York

Respostas:


9

Acho que seu colega estava certo: você está projetando seus casos de exceção com base em como é simples implementar dentro da hierarquia, não com base nas necessidades de tratamento de exceções do código do cliente.

Com um tipo de exceção e uma enumeração para a condição de erro (sua solução), se o código do cliente precisar lidar com casos de erro únicos (por exemplo my_errc::error_x) , eles deverão escrever um código como este:

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

Com vários tipos de exceção (com uma base comum para toda a hierarquia), você pode escrever:

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

onde as classes de exceção são assim:

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

Ao escrever uma biblioteca, o foco deve estar na facilidade de uso, não (necessariamente) na implementação interna.

Você só deve comprometer a facilidade de uso (como será o código do cliente) quando o esforço de fazê-lo corretamente na biblioteca for proibitivo.


0

Concordo com seus revisores e com @utnapistim. Você pode usar a system_errorabordagem ao implementar coisas de plataforma cruzada quando alguns erros requerem tratamento especial. Mas, mesmo neste caso, não é uma boa solução, mas uma solução menos maligna.

Mais uma coisa. Ao criar uma hierarquia de exceções, não a torne muito profunda. Crie apenas as classes de exceção que podem ser processadas pelos clientes. Na maioria dos casos, eu uso apenas std::runtime_errore std::logic_error. Atiro std::runtime_errorquando algo sai errado e não consigo fazer nada (o usuário ejeta o dispositivo do computador, ele esqueceu o aplicativo ainda em execução) e std::logic_errorquando a lógica do programa é interrompida (o usuário tenta excluir o registro do banco de dados que não existe, mas antes da operação de exclusão ele pode verificá-lo, para que ele obtenha erro de lógica).

E como desenvolvedor de bibliotecas, pense nas necessidades de seus usuários. Tente usá-lo você mesmo e pense se é um conforto para você. Você pode explicar sua posição aos revisores com exemplos de código.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.