Русский
Русский
English
Статистика
Реклама

Can_throw или не can_throw?


Исключения являются частью языка C++. Неоднозначной его частью. Кто-то их принципиально не использует. Вот вообще не использует. От слова совсем. Но не мы. Поскольку считаем их весьма полезной штукой, существенно повышающей надежность кода.


К сожалению, далеко не везде исключения можно задействовать. Во-первых, исключения не бесплатны и, во-вторых, не всякий код способен "пережить" возникновение исключений.


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


По большому счету, у нас в распоряжении есть только спецификатор noexcept. Штука полезная, конечно, но недостаточная.


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


Преамбула


В C++ есть спецификатор noexcept. Видя отметку noexcept в декларации функции/метода разработчик может понять, что вызывая эту функцию/метод исключений можно не ждать. Соответственно, используя noexcept функции/методы кода можно безопасно писать код для контекстов, в которых бросать исключения нельзя (деструкторы классов, операции swap, передаваемые в C-шный код callback-и и т.д.).


Однако, отметка noexcept хорошо видна лишь когда ты изучаешь декларации функций/методов. Но когда есть код, в котором вызывается какая-то функция/метод, то сразу не поймешь, ждать ли здесь исключений или нет. Вот, например:


void some_handler::on_read_result(    const asio::error_code & ec,    std::size_t bytes_transferred){    if(!ec)    {        m_data_size = bytes_transferred;        handle_data();    }    else    {...}}

Не имея перед глазами декларации handle_data нельзя просто так определить, могут ли тут вылетать наружу исключения или не могут.


Так что спецификатор noexcept решает только первую часть проблемы: позволяет понять при написании кода можно ли вызывать конкретную функцию/метод не ожидая вылета наружу исключения.


Тогда как вторая часть это убедится в том, бросает или не бросает исключения уже написанный ранее кусок кода, в котором вызываются те или иные методы. И вот тут лично мне не хватает наличия в C++ чего-то вроде noexcept-блока. Я бы хотел написать кусок кода и поместить этот кусок в noexcept-блок. Что-то типа:


void some_handler::on_read_result(    const asio::error_code & ec,    std::size_t bytes_transferred){    noexcept    {        if(!ec)        {            m_data_size = bytes_transferred;            handle_data();        }        else        {...}    }}

А нужен этот блок чтобы получить проверку со стороны компилятора. Если в noexcept-блоке выполняются только noexcept-операции, то все хорошо. Но если какое-то из действий может бросить исключение, то компилятор выдает предупреждение, а лучше ошибку.


К сожалению, такого noexcept-блока в C++ пока нет. А раз нет, то приходится выкручиваться подручными средствами. Об одном таком самодельном средстве уже рассказывалось некоторое время назад. Сегодня же хочется рассказать о другом слепленном на коленке велосипеде, который несколько облегчил жизнь.


Проблема


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


Прежде всего это callback-и, которые выступают в роли completion-handler-ов для Asio. Выпускать исключения из таких callback-ов нельзя, т.к. Asio эти исключения не ловит и не обрабатывает. Соответственно, вылет исключения из completion-handler-а это крах приложения.


Так же есть callback-и, которые отдаются в библиотеку на чистом Си. И, соответственно, оттуда так же нельзя выбрасывать исключения.


Поэтому внутри callback-а, который отдается в Asio или в C-шную библиотеку, нужно сделать try/catch, внутри которого будут выполняться нужные приложению действия, а вот выброшенные исключения будут перехватываться:


void some_handler::on_read_result(    const asio::error_code & ec,    std::size_t bytes_transferred){    try    {        handle_read_result(ec, bytes_transferred); // Основные действия.    }    catch(...)    {        // Хотя бы просто "проглотить" исключение.    }}

Решение очевидное, но, к сожалению, ничто не мешает невнимательному (или уставшему) разработчику написать callback без try/catch и вызвать там метод handle_read_result. И компилятор тут нам ничем не поможет.


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


Решение в виде маркера can_throw


Решение было найдено в виде специального маркера can_throw, который передается аргументом во все прикладные функции/методы. Поэтому, если функция получает аргумент типа can_throw, то она может бросать исключения. А также вызывать другие функции/методы, которые получают can_throw.


Соответственно, если в каком-то callback-е нам приходится вызывать функцию/метод, которые требуют аргумента can_throw, то нам нужно позаботится о перехвате и обработке исключений.


А позаботится об этом нас заставит сам компилятор, т.к. маркер can_throw нельзя просто так создать и отдать в вызываемую функцию/метод. Т.е. мы не можем написать вот так:


void some_handler::handle_read_result(    can_throw_t can_throw,    const asio::error_code & ec,    std::size_t bytes_transferred){    ... // Прикладная обработка которая может бросать исключения.}void some_handler::on_read_result(    const asio::error_code & ec,    std::size_t bytes_transferred){    // Вот так быть не должно!    handle_read_result(can_throw_t{}, ec, bytes_transferred);}

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


class can_throw_t{    friend class exception_handling_context_t;    can_throw_t() noexcept = default;public:    ~can_throw_t() noexcept = default;    can_throw_t( const can_throw_t & ) noexcept = default;    can_throw_t( can_throw_t && ) noexcept = default;    can_throw_t &    operator=( const can_throw_t & ) noexcept = default;    can_throw_t &    operator=( can_throw_t && ) noexcept = default;};

Т.е. кто угодно может копировать и перемещать экземпляры типа can_throw_t, но вот создавать эти экземпляры "могут не только лишь все" (с). Для того, чтобы получить экземпляр can_throw_t следует сперва создать экземпляр типа exception_handling_context_t:


class exception_handling_context_t{public:    can_throw_t    make_can_throw_marker() const noexcept { return {}; }};

а затем воспользоваться методом make_can_throw_marker()


void some_handler::on_read_result(    const asio::error_code & ec,    std::size_t bytes_transferred){    try    {        exception_handling_context_t ctx;        handle_read_result(ctx.make_can_throw_marker(), ec, bytes_transferred);    }    catch(...)    {}}

Да, при этом ничто не запрещает создавать экземпляры exception_handling_context_t и без использования блоков try/catch. И можно было бы попробовать сделать более железобетонное решение. Например, функцию wrap_throwing_action, которая бы получала на вход лямбду, а внутри имела бы блок try, внутри которого бы лямбда и вызывалась. Что-то вроде:


class can_throw_t{    // Разрешаем создание can_throw только внутри    // шаблонной функции wrap_throwing_action.    template<typename Lambda>    friend void wrap_throwing_action(Lambda &&);    can_throw_t() noexcept = default;public:    ... // Все как показано выше.};template< typename Lambda >void wrap_throwing_action(Lambda && lambda){    try    {        lambda(can_throw_t{});    }    catch(...)    {}}

Можно было бы и так.


Но пока мы ограничились именно показанными выше тривиальными реализациями can_throw_t и exception_handling_context_t.


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


Отчасти потому, что какие-то функции/методы нужно вызывать не только из callback-ов, но и из конструкторов объектов. А в конструкторах исключения разрешены, посему и создавать внутри тела конструктора дополнительный try нет смысла. Гораздо проще внутри конструктора объявить временный exception_handling_context_t и вызывать нужную функцию:


some_handler::some_handler(    std::vector<std::byte> initial_data,    std::size_t initial_data_size)    : m_data{std::move(initial_data)}    , m_data_size{initial_data_size}{    exception_handling_context_t ctx;    handle_data(ctx.make_can_throw_marker());}...void some_handler::handle_read_result(    can_throw_t can_throw,    const asio::error_code & ec,    std::size_t bytes_transferred){    if(!ec)    {        m_data_size = bytes_transferred;        handle_data(can_throw);    }    else    {        ...    }}...void some_handler::handle_data(can_throw_t){    ... // Прикладная обработка данных.}

Отчасти еще и потому, что для разных ситуаций нужны разные действия в catch: где-то проблемы логируются, где-то "проглатываются" (но при этом из callback-а возвращается код ошибки, а не положительный результат). Попытка запихнуть эти особенности обработки исключений в wrap_throwing_action только усложнила бы реализацию wrap_throwing_action.


Общие впечатления


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


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


Во-первых, это увеличение объема кода за счет маркеров can_throw. Т.е., с одной стороны, глядя на код сразу видишь, кому разрешено бросать исключения. Но, с другой стороны, во многих функциях/методах появляется дополнительный параметр. И требуется некоторая привычка, чтобы не обращать на него внимание, если хочется разобраться с тем, что и как делает метод.


Во-вторых, накладные расходы на передачу маркера can_throw вниз по стеку вызовов не оценивались. В нашем конкретном случае такие накладные расходы, если они и есть, роли не играют. Т.к. callback-и, в которых can_throw создаются, вызываются ну максимум несколько десятков тысяч раз в секунду. И передача экземпляров can_throw внутри callback-а это просто копейки по сравнению с выполняемой callback-ами прикладной работой (не говоря уже о стоимости операций, приводящих к вызову callback-ов).


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


Поэтому, как минимум, два вышеозначенных момента нужно иметь в виду тем, кто захочет применить подход с маркерами can_throw в своем коде.


Заключение


В данной статье я попытался поделится нашим свежим опытом и решением, которое несколько облегчило нам жизнь, при разработке нового кода на C++.


Но само это решение не было придумано нами. Насколько я помню, подобный подход описывался на каком-то из форумов (вроде бы это был RSDN) лет эдак 15 назад. Так что мы здесь ничего не изобретали, а лишь вспомнили про то, что кто-то придумал много лет назад.


Конечно же, было бы лучше иметь более продвинутые средства контроля за выбросом исключений в С++. Тогда бы не пришлось прибегать к велосипедам типа can_throw. Но пока есть лишь то, что есть :( И для повышения степени доверия к коду приходится собирать на коленке собственные велосипеды.

Источник: habr.com
К списку статей
Опубликовано: 28.07.2020 14:05:45
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

C++

Ненормальное программирование

Программирование

C++11

Категории

Последние комментарии

© 2006-2021, personeltest.ru