Pull to refresh

Comments 22

У Вас как-то все сделано однобоко — заточено на погашении исключения. Нигде в коде не увидел у вас работы с объектом ошибки. Думаю это не верный подход.

Так ведь и статья не о том, как обрабатывать исключения. А о том, как при чтении кода быстро понять, что следует вызывать только внутри try. Ну и о том, как получить хотя бы некоторую помощь в этом со стороны компилятора.


Поэтому в статье нет ни одного реального примера try и catch. Хотя о том, что catch-и с разными действиями в проекте присутствуют в статье упоминается.

К сожалению, никто не мешает написать метод, не принимающий can_throw, но бросающий исключение.
Мне кажется, было бы проще обязать всех ставить к-нить макросы вокруг коллбэков, вроде
BEGIN_EXCEPTION_HANDLER {
// work
} END_EXCEPTION_HANDLER

Гораздо проще, имхо, обязать всех не передавать callback-и в Asio напрямую, а вызывать специальный метод для создания обертки вокруг callback-а (а сами прикладные действия отдавать в этот метод посредством лямбды). Что, собственно, у нас и происходит.


Но т.к. мы не можем с помощью компилятора проверить, что все callback-и создаются именно таким образом (а писать какие-то плагины для компилятора или делать собственные анализаторы на базе clang-а в наши планы не входит), то захотелось иметь дополнительную степень безопасности. Если так можно сказать.


Так же и с макросами BEGIN_EXCEPTION_HANDLER/END_EXCEPTION_HANDLER. Обязать их расставлять можно. А вот как автоматически контролировать выполнение этого требования?

Мне кажется, выцепить глазом отсутствие макросов намного проще, особенно если взаимодействие с asio вынесено в файлы отдельных типов (как в mvc — модель отдельно, контроллер отдельно… asio отдельно). А как глазами выцепить, что в функции нужен лишний параметр, я даже не представляю
А как глазами выцепить, что в функции нужен лишний параметр, я даже не представляю

Так C++ же статически типизированный язык. Если каких-то параметров не будет хватать или у них типы не совпадают, то компиляция не пройдет.

Не понял насчет статической типизации. Ну вот например есть такая функция
optional<S> parse(json) {
    S result;
    if (!json.contains("id")) 
        return nullopt;
    result.id = json.get<int>("id");
    if (!json.contains("value"))
        return nullopt;
    result.value = std::stol(json.get<string>("value"));
    if (!json.contains("type"))
        return nullopt;
    result.type = json.get<string>("type");
    return result;
}


Нужно ли в него вставлять этот дополнительный параметр?

Э… Вы ставите меня в тупик. Я не понимаю, что это за функция и какое отношение она имеет к описанному в статье.


Могу предположить, что вам нужно вызвать такую функцию из какого-то callback-а, который отдается в Asio. Если так, то все будет зависеть от того, что еще делается в callback-е.


Например, у нас это могло бы выглядеть так:


m_connection.async_read_some(asio::buffer(m_data),
   with<const asio::error_code &, std::size_t>().make_handler(
      [this](can_throw_t can_throw,
         const asio::error_code & ec,
         std::size_t bytes_transferred)
      {
         if(!ec) {
            m_data_size = bytes_transferred;
            auto json = try_extract_payload();
            auto data = parse(json);
         }
      });

То, в принципе, в parse необязательно добавлять еще один аргумент. Т.к. parse уже вызывается в контексте, где try/catch есть.


Однако, если вы все-таки в parse добавите параметр can_throw, то даже если вы (или тот, кто будет дописывать код после вас) вдруг напишите async_read_some без создания специального врапера:


m_connection.async_read_some(asio::buffer(m_data),
   [this](const asio::error_code & ec,
         std::size_t bytes_transferred)
   {
      if(!ec) {
         m_data_size = bytes_transferred;
         auto json = try_extract_payload();
         auto data = parse(json);
      }
   });

То компилятор даст вам по рукам.

Уу, тоесть у вас то, добавлять гуард к функции или не добавлять, еще зависит от того, из какого контекста функция вызывается? Тогда уж точно макросы проще
Уу, тоесть у вас то, добавлять гуард к функции или не добавлять, еще зависит от того, из какого контекста функция вызывается?

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

Можно сказать, что у вас логически два типа кода — современный с эксепшионами и легаси. Вы смешали два типа кода и тепер просите компилятор контролировать границу перехода. Но для компилятора раздела на современный код и легаси нет и он вам тут нетпомогает. Решение этой проблемы — создание обверти легаси API. Блаодаря обвертке вы как раз и не сможете выстрелить себе в ногу при всем желании. :)

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


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


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

Я использовал описание кода легаси/не легаси как логическое разделение кода. К реальной старости кода это не относится. Я веду к тому, что у вас есть четкие точки перехода между разными типами кода. Так вот защиту нужно не размызвать тонким слоем, а контролироват границу. Например, функция asio, принимающая каллбэк — делаем обвертку в которой оборачиваем пришедший каллбэк своим кодом, ловящим наши эксепшионы. Что мы получаем:
— нельзя забыть про эксепшионы
— наш код чистый
— profit :)
А в вашем случае вы переложили контроль границы на все места перехода этой границы, и ясное дело что это еще и дублирование кода контроля, в котором можно ошибиться, или вообще не неапсать.
Например, функция asio, принимающая каллбэк — делаем обвертку в которой оборачиваем пришедший каллбэк своим кодом, ловящим наши эксепшионы.

Делать обертки вокруг чужих библиотек — так себе занятие. Вот та же Asio. Непростая библиотека с продвинутым API сама по себе. И, по вашему, нам нужно сделать собственные обертки над asio::ip::tcp::socket и такими его методами, как async_read_some/async_write_some?

Делать обертки вокруг чужих библиотек — так себе занятие. Вот та же Asio. Непростая библиотека с продвинутым API сама по себе. И, по вашему, нам нужно сделать собственные обертки над asio::ip::tcp::socket и такими его методами, как async_read_some/async_write_some?

Asio действительно непростая библиотека, и в ней есть даже небольшая поддержка аспектно-ориентированного программирования, сосредоточенная как раз вокруг обработчиков. Так что оборачивать всё совсем не нужно, вызовы обработчиков можно контролировать. И это расширяемо в том плане, что не важно обработчик чего это — события на сокете, таймере или вообще кто-то написал дополнение для asio с асинхронной работой с какими-то другими объектами.

Правда, для этого обработчики должны иметь некий пользовательский тип. Но, по-моему, это и так всегда полезно — хоть для какой-то отладки вызова обработчиков, хоть для выделения памяти для максимальной производительности.
Так что оборачивать всё совсем не нужно, вызовы обработчиков можно контролировать.

Я не настолько хорошо владею Asio, чтобы понять, откуда берется my_handler в примере по ссылке:


class my_handler;

template <typename Function>
void asio_handler_invoke(Function function, my_handler* context)
{
  context->strand_.dispatch(function);
}

Это будет указатель на экземпляр, который передается, скажем, в async_read_some? Типа такого:


struct my_read_handler {
  void operator()(const asio::error_code & ec, std::size_t bytes) {...}
  ...
};
...
some_connection.async_read_some(asio::buffer(...), my_read_handler{...});
Это будет указатель на экземпляр, который передается, скажем, в async_read_some?

Да. Точнее, конечно, на копию переданного. Он нужен для разрешения перегрузки. А Function заботится о том, чтобы передать в него нужные аргументы (код ошибки, количество прочитанных байтов и т.д.).

Ясно. Спасибо, не знал про эту возможность.

Но вот Asio понятия не имеет, что делать с исключениями, которые вылетают из completion-handler-а. Следовательно, исключения из переданных в Asio completion-handler-ов выпускать нельзя.

Asio отнюдь не возражает против исключений из обработчиков, и относится к ним вполне толерантно.

Другое дело, конечно, что в catch блоке мы окажемся уж слишком оторванными от контекста, и будет сложно правильно среагировать — удалить какие-нибудь сессии/соединения или совершить попытку ещё раз.
Asio отнюдь не возражает против исключений из обработчиков, и относится к ним вполне толерантно.

А можно подробнее. Что значит "относится толерантно"?

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

Теперь понял о чем вы говорите. Значит у нас специфика такая, что если выпущенное из completion-handler-а исключение покинуло Asio и прервало io_context::run, то дело плохо.

Sign up to leave a comment.

Articles