Diseñando clases de excepción


9

Estoy codificando una pequeña biblioteca y estoy teniendo problemas para diseñar el manejo de excepciones. Debo decir que estoy (todavía) confundido por esta característica del lenguaje C ++ e intenté leer lo más posible sobre el tema para comprender qué tendría que hacer para trabajar adecuadamente con las clases de excepción.

Decidí usar un system_errortipo de enfoque inspirado en la implementación STL de la future_errorclase.

Tengo una enumeración que contiene los códigos de error:

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

y una sola clase de excepción (respaldada por un error_categorytipo de estructuras y todo lo demás que necesita el 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 { };
    }

Solo tengo un pequeño número de situaciones en las que lanzo excepciones ilustradas por la enumeración del código de error.

Lo anterior no se sentó bien con uno de mis críticos. Opinaba que debería haber creado una jerarquía de clases de excepción con una clase base derivada std::runtime_errorporque tener el código de error incrustado en la condición mezcla cosas, excepciones y códigos de error, y sería más tedioso tratar un punto. de manejo; la jerarquía de excepción también permitiría una fácil personalización del mensaje de error.

Uno de mis argumentos fue que quería mantenerlo simple, que mi biblioteca no necesitaba lanzar múltiples tipos de excepciones y que la personalización también es fácil en este caso, ya que se maneja automáticamente: error_codetiene un error_categoryasociado que traduce el código para el mensaje de error apropiado.

Tengo que decir que no defendí bien mi elección, testimonio del hecho de que todavía tengo algunos malentendidos con respecto a las excepciones de C ++.

Me gustaría saber si mi diseño tiene sentido. ¿Cuáles serían las ventajas del otro método sobre el que elegí, ya que tengo que admitir que no veo eso también? ¿Qué puedo hacer para mejorar?


2
En principio, estoy de acuerdo con su revisor (mezclar códigos de error y excepciones no es realmente tan útil). Pero a menos que tenga una gran biblioteca que tenga una gran jerarquía tampoco es útil. Una excepción base que contiene una cadena de mensaje, solo tiene excepciones separadas si el receptor de la excepción puede potencialmente usar la unicidad de la excepción para solucionar el problema.
Martin York

Respuestas:


9

Creo que su colega tenía razón: está diseñando sus casos de excepción en función de lo sencillo que es implementar dentro de la jerarquía, no en función de las necesidades de manejo de excepciones del código del cliente.

Con un tipo de excepción y una enumeración para la condición de error (su solución), si el código del cliente necesita manejar casos de error únicos (por ejemplo my_errc::error_x), deben escribir 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
    }
}

Con múltiples tipos de excepción (que tienen una base común para toda la jerarquía), puede escribir:

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

donde las clases de excepción se ven así:

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

Al escribir una biblioteca, el enfoque debe estar en su facilidad de uso, no (necesariamente) la facilidad de la implementación interna.

Solo debe comprometer la facilidad de uso (cómo se verá el código del cliente) cuando el esfuerzo de hacerlo correctamente en la biblioteca es prohibitivo.


0

Estoy de acuerdo con sus revisores y con @utnapistim. Puede usar el system_errorenfoque cuando implementa cosas multiplataforma cuando algunos errores requieren un manejo especial. Pero incluso en este caso, no es una buena solución, sino una solución menos mala.

Una cosa más. Cuando cree una jerarquía de excepciones, no la haga muy profunda. Cree solo esas clases de excepción, que pueden ser procesadas por los clientes. En la mayoría de los casos solo uso std::runtime_errory std::logic_error. Lanzo std::runtime_errorcuando algo sale mal y no puedo hacer nada (el usuario expulsa el dispositivo de la computadora, olvidó que la aplicación aún se está ejecutando), y std::logic_errorcuando la lógica del programa no funciona (el usuario intenta eliminar el registro de la base de datos que no existe, pero antes de eliminar la operación, puede verificarlo, para que obtenga un error lógico).

Y como desarrollador de bibliotecas, piense en las necesidades de sus usuarios. Trate de usarlo usted mismo y piense, si es cómodo para usted. Luego, puede explicar su posición a sus revisores con ejemplos de código.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.