Проектирование классов исключений

9

Я пишу небольшую библиотеку, и у меня возникли проблемы с проектированием обработки исключений. Я должен сказать, что меня (все еще) смущает эта особенность языка C ++, и я постарался прочитать как можно больше по этому вопросу, чтобы понять, что мне нужно сделать, чтобы правильно работать с классами исключений.

Я решил использовать system_errorтип подхода, основанный на реализации future_errorкласса STL .

У меня есть перечисление, содержащее коды ошибок:

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

и один класс исключений (поддерживается error_categoryтипом структур и всем остальным, что необходимо 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 { };
    }

У меня есть только небольшое количество ситуаций, в которых я выкидываю исключения, проиллюстрированные перечислением кода ошибки.

Вышесказанное не очень понравилось одному из моих рецензентов. Он считал, что я должен был создать иерархию классов исключений с базовым классом, полученным из- std::runtime_errorза того, что код ошибки, встроенный в условие, смешивает вещи - исключения и коды ошибок - и было бы более утомительно разбираться с точкой обработки; иерархия исключений также позволит легко настроить сообщение об ошибке.

Один из моих аргументов состоял в том, что я хотел сохранить простоту, чтобы моей библиотеке не нужно было генерировать несколько типов исключений, и что в этом случае настройка также проста, так как она обрабатывается автоматически - с error_codeней error_categoryсвязана функция, которая переводит код для правильного сообщения об ошибке.

Я должен сказать, что я не очень хорошо отстаивал свой выбор, свидетельствуя о том, что у меня все еще есть некоторые недоразумения относительно исключений C ++.

Я хотел бы знать, имеет ли мой дизайн смысл. Каковы будут преимущества другого метода перед тем, который я выбрал, поскольку должен признать, что я тоже не вижу этого? Что я могу сделать, чтобы улучшить?

celavek
источник
2
Я склонен в принципе согласиться с вашим рецензентом (смешивать коды ошибок и исключений не очень полезно). Но если у вас нет огромной библиотеки с большой иерархией, это тоже не полезно. Базовое исключение, содержащее строку сообщения, может иметь отдельные исключения, только если перехватчик исключения может потенциально использовать уникальность исключения для решения проблемы.
Мартин Йорк

Ответы:

9

Я думаю, что ваш коллега был прав: вы разрабатываете свои исключения, основываясь на том, насколько просто это реализовать в иерархии, а не на потребностях клиента в обработке исключений.

С одним типом исключения и перечислением для условия ошибки (ваше решение), если клиентский код должен обрабатывать отдельные случаи ошибок (например, my_errc::error_x), он должен написать код, подобный этому:

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

С несколькими типами исключений (имеющих общую базу для всей иерархии) вы можете написать:

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

где классы исключений выглядят так:

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

При написании библиотеки основное внимание следует уделять простоте ее использования, а не (обязательно) легкости внутренней реализации.

Вы должны ставить под угрозу простоту использования (как будет выглядеть клиентский код) только тогда, когда попытка сделать это правильно в библиотеке является непомерной.

utnapistim
источник
0

Я согласен с вашими рецензентами и с @utnapistim. Вы можете использовать system_errorподход, когда реализуете кроссплатформенные вещи, когда некоторые ошибки требуют специальной обработки. Но даже в этом случае это не хорошее решение, а менее злое решение.

Еще кое-что. Когда создаете иерархию исключений, не делайте это очень глубоко. Создавайте только те классы исключений, которые могут обрабатываться клиентами. В большинстве случаев я использую только std::runtime_errorи std::logic_error. Я бросаю, std::runtime_errorкогда что-то идет не так, и я ничего не могу сделать (пользователь извлекает устройство из компьютера, он забыл, что приложение все еще работает), и std::logic_errorкогда логика программы нарушена (Пользователь пытается удалить запись из базы данных, которая не существует, но перед операцией удаления можете проверить, чтобы он получил логическую ошибку).

И, как разработчик библиотеки, подумайте о потребностях ваших пользователей. Попробуйте использовать его сами и подумайте, будет ли это комфортно для вас. Чем вы можете объяснить свою позицию рецензентам с помощью примеров кода.


источник