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


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.