Проектування класів виключень


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 ++.

Я хотів би знати, чи має сенс моя конструкція. Які б були переваги іншого методу перед тим, який я обрав, оскільки, маю визнати, я також не бачу цього? Що я міг би зробити для покращення?


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

Писаючи бібліотеку, слід зосередити увагу на простоті використання, а не (обов'язково) на простоті внутрішньої реалізації.

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


0

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

І ще одна річ. Створюючи ієрархію винятків, не робіть її дуже глибокою. Створіть лише ті класи винятків, які можуть бути оброблені клієнтами. У більшості випадків я використовую тільки std::runtime_errorі std::logic_error. Я кидаю, std::runtime_errorколи щось піде не так, і я нічого не можу зробити (користувач виймає пристрій з комп'ютера, він забув, що програма все ще працює), і std::logic_errorколи логіка програми порушена (Користувач намагається видалити запис із бази даних, яка не існує, але перед видаленням операції він може це перевірити, тому він отримає логічну помилку).

І як розробник бібліотеки, подумайте про потреби своїх користувачів. Спробуйте використовувати його самостійно і подумайте, чи це вам комфорт. Тоді ви можете пояснити свою позицію рецензентам на прикладах коду.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.