суббота, 25 июля 2020 г.

[prog.c++] Пример использования шаблонов для создания C-шных коллбэков из методов C++-ных классов

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

Итак, есть известная чисто-С-шная библиотека http_parser, которую много кто использует (в том числе и мы в RESTinio). Для работы с ней нужно создать у себя в программе экземпляр типа http_parser и экземпляр типа http_parser_settings. Где http_parser_settings должен содержать набор указателей на С-шные коллбэки, которые будут вызываться во время парсинга HTTP-сообщений.

Возникла задача сделать так, чтобы коллбэками для http_parser выступали нестатические методы C++ ного класса.

Решалась эта задача в два этапа.

На первом этапе была создана простая шаблонная функция-обертка, которая производит вызов C++-ного метода (при этом предполагается, что в http_parser.data лежит указатель на C++-ный объект, у которого методы нужно вызывать):

template<
   typename Handler,
   typename... Args >
[[nodiscard]]
int
wrap_http_parser_callback(
   http_parser * parser,
   int (Handler::*callback)( utils::can_throw_t, Args... ),
   Args ...args ) noexcept
{
   auto * handler = reinterpret_cast<Handler *>(parser->data);
   try
   {
     utils::exception_handling_context_t ctx;

      return (handler->*callback)( ctx.make_can_throw_marker(),
            std::forward<Args>(args)... );
   }
   catch(...) {}

   return -1;
}

Предполагалось, что эта шаблонная обертка будет вызываться из лямбды без списка захвата (такая лямбда автоматически преобразуется к указателю на С-шную функцию). Что позволяло оформлять коллбэки для http_parser-а следующим образом:

void
data_processor_t::initialize_http_parser_settings()
{
   http_parser_settings_init( &m_http_parser_settings );

   m_http_parser_settings.on_message_begin =
      []( http_parser * parser )
      {
         return wrap_http_parser_callback( parser,
               &data_processor_t::on_message_begin );
      };

   m_http_parser_settings.on_url =
      []( http_parser * parser, const char * data, std::size_t size )
      {
         return wrap_http_parser_callback( parser,
               &data_processor_t::on_url,
               data, size );
      };

   ...
}

На этом, в принципе, можно было бы и остановится. Но...

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

Хотелось бы иметь более простой способ. Что-то вроде:

   m_http_parser_settings.on_message_begin =
      какая-то-волшебная-функция( &data_processor_t::on_message_begin );

Поэтому на втором этапе была разработана шаблонная функция make_http_parser_callback(), которая и сделала возможной лаконичную запись вида:

void
data_processor_t::initialize_http_parser_settings()
{
   http_parser_settings_init( &m_http_parser_settings );

   m_http_parser_settings.on_message_begin =
      make_http_parser_callback< &data_processor_t::on_message_begin >();

   m_http_parser_settings.on_url =
      make_http_parser_callback< &data_processor_t::on_url >();

И как раз о том, что скрывается под make_http_parser_callback речь и пойдет ниже.

Прежде всего, make_http_parser_callback должен возвращать указатель на С-шную функцию. Другими словами, лямбду без списка захвата. Поскольку только такая лямбда штатно преобразуется к указателю на С-шную функцию.

Но если внутри make_http_parser_callback создается лямбда без списка захвата, то как внутри этой лямбды "захватить" указатель на метод класса?

Если передавать указатель на метод класса обычным параметром, то никак. Поэтому make_http_parser_callback не может иметь вид:

templatetypename Handler, typename... Args >
auto
make_http_parser_callback( int (Handler::*method)(Args...) );

поскольку параметр method в таком случае никак в лямбду без списка захвата не засунешь.

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

Благо, в C++17 передать указатель на метод параметром шаблона проще простого: в C++ можно указывать template<auto Param>. Чем я и воспользовался.

В самом простом варианте make_http_parser_callback выглядел так (это не точная цитата из кода, а воспроизведение по памяти):

templateauto Callback >
auto
make_http_parser_callback_no_data()
{
   return []( http_parser * parser )
      {
         return wrap_http_parser_callback( parser, Callback );
      };
}

templateauto Callback >
auto
make_http_parser_callback_with_data()
{
   return []( http_parser * parser, const char * data, std::size_t size )
      {
         return wrap_http_parser_callback( parser,
               Callback,
               data, size );
      };
}

Вместо одной функции make_http_parser_callback появилось две, поскольку коллбэки для http_parser на данный момент бывают двух типов: без указателя на данные и с оным указателем (+размер данных). И когда мы имеем просто auto Callback то просто так не поймешь, какой именно тип лямбды нужно создавать внутри make_http_parser_callback. Поэтому самый простой путь -- это две разные функции make_http_parser_callback_*.

Но две разные функции -- это не очень удобно. Можно легко ошибиться и написать так:

void
data_processor_t::initialize_http_parser_settings()
{
   http_parser_settings_init( &m_http_parser_settings );

   m_http_parser_settings.on_message_begin =
      make_http_parser_callback_no_data<
         &data_processor_t::on_message_begin >();

   m_http_parser_settings.on_url =
      make_http_parser_callback_no_data<
         &data_processor_t::on_url >();

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

Поэтому захотелось сделать более продвинутый вариант make_http_parser_callback, который сам бы разобрался, какой формат у Callback-а и что с этим форматом делать.

В результате нескольких итераций получился следующий вариант единственной функции make_http_parser_callback:

templateauto Callback >
[[nodiscard]]
auto
make_http_parser_callback() noexcept
{
   using detector = http_parser_callback_kind_detector< decltype(Callback) >;

   if constexpr( detector::with_data )
      return []( http_parser * parser, const char * data, std::size_t size )
         {
            return wrap_http_parser_callback( parser, Callback, data, size );
         };
   else
      return []( http_parser * parser )
         {
            return wrap_http_parser_callback( parser, Callback );
         };
}

Где самое интересное (ну кроме if constexpr, который стал доступен в C++17) -- это http_parser_callback_kind_detector:

templatetypename T >
struct http_parser_callback_kind_detector;

templatetypename Handler >
struct http_parser_callback_kind_detector<
   int (Handler::*)( utils::can_throw_t ) >
{
   static constexpr bool with_data = false;
};

templatetypename Handler >
struct http_parser_callback_kind_detector<
   int (Handler::*)( utils::can_throw_t, const char *, std::size_t ) >
{
   static constexpr bool with_data = true;
};

Здесь как раз посредством специализации шаблонного класса выполняется деконструкция указателя на метод класса. И, в зависимости от того, что это за метод, происходит определение значения with_data.


Вот такой вот пример ситуации, где знание о том, как разобраться с форматом указателя на функцию/метод помогает сократить объем кода. При сохранении всех достоинств статической типизации и отловом ошибок типизации в compile-time.

Примечательно, что когда я начинал писать заметку, реализация внутренней кухни make_http_parser_callback была сложнее. Но по ходу написания в попытках объяснить текущее решение обнаружилось несколько способов упроситить и сократить код. Так что не знаю, насколько эта заметка окажется интересной/полезной для читателей блога. Но для меня она уже окупилась ;)


По поводу маркера can_throw_t, который можно увидеть в приведенном коде, я хочу написать отдельную статью. Скорее всего для Хабра. Но не знаю, когда для этого представится время :(

Комментариев нет: