В предыдущей заметке был приведен перечень интересных статей о том, как посредством шаблонов разбираться с указателями на функции. Сегодня я попробую привести пример того, как что-то подобное мне пригодилось на практике.
Итак, есть известная чисто-С-шная библиотека 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 не может иметь вид:
template< typename Handler, typename... Args > auto make_http_parser_callback( int (Handler::*method)(Args...) ); |
поскольку параметр method в таком случае никак в лямбду без списка захвата не засунешь.
Поэтому нужно, чтобы указатель на метод класса был параметром шаблона. Тогда его можно задействовать внутри лямбды.
Благо, в C++17 передать указатель на метод параметром шаблона проще простого: в C++ можно указывать template<auto Param>. Чем я и воспользовался.
В самом простом варианте make_http_parser_callback выглядел так (это не точная цитата из кода, а воспроизведение по памяти):
template< auto Callback > auto make_http_parser_callback_no_data() { return []( http_parser * parser ) { return wrap_http_parser_callback( parser, Callback ); }; } template< auto 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:
template< auto 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:
template< typename T > struct http_parser_callback_kind_detector; template< typename Handler > struct http_parser_callback_kind_detector< int (Handler::*)( utils::can_throw_t ) > { static constexpr bool with_data = false; }; template< typename 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, который можно увидеть в приведенном коде, я хочу написать отдельную статью. Скорее всего для Хабра. Но не знаю, когда для этого представится время :(
Комментариев нет:
Отправить комментарий