Хабрахабр

RPC — повод попробовать новое в C++ 14 / 17

Несколько лет назад разработчики на C++ получили долгожданный стандарт C++ 11, принесший много нового. И у меня был интерес быстрее перейти к его использованию в повседневно решаемых задачах. Перейти к C++ 14 и 17 такого не было. Казалось, нет того набора фич, который бы заинтересовал. Весной я все же решил посмотреть на новшества языка и что-нибудь попробовать. Чтобы поэкспериментировать с новшествами нужно было придумать себе задачу. Долго думать не пришлось. Решено написать свое RPC с пользовательскими структурами данных в качестве параметров и без использования макросов и кодогенерации — все на C++. Это удалось благодаря новым возможностям языка.

К концу же удалось дописать пост на Хабр. Идея, реализация, фидбэк с Reddit, доработки — все появилось весной, начале лета.

Возможно, материал поста Вам поможет определиться с целью, методами, средствами и принять решение в пользу готового или что-то реализовывать самостоятельно…
Вы задумались о собственном RPC?

RPC (remote procedure call) — тема не новая. Существует множество реализаций на разных языках программирования. В реализациях используются различные форматы данных и виды транспорта. Все это можно отразить несколькими пунктами:

  • Сериализация / десериализация
  • Транспорт
  • Выполнение удаленного метода
  • Возврат результата

Реализация определяется желаемой целью. Например, можно задаться целью обеспечить высокую скорость вызова удаленного метода и пожертвовать удобством использования или наоборот обеспечить максимальный комфорт написания кода, возможно, немного потеряв в производительности. Цели и инструменты разные… Мне хотелось комфорта и приемлемой производительности.
Ниже приведено несколько шагов реализации RPC на C++ 14 / 17, и сделаны акценты на некоторые новшества языка, ставшие причиной появления этого материала.

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

Сериализация

Перед тем, как начать писать код сформирую задачу:

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

Ниже приведен код упрощенного строкового сериализатора.

string_serializer

namespace rpc::type
{
using buffer = std::vector<char>;
} // namespace rpc::type namespace rpc::packer
{ class string_serializer final
{
public: template <typename ... T> type::buffer save(std::tuple<T ... > const &tuple) const ); return {begin(str), end(str)}; } template <typename ... T> void load(type::buffer const &buffer, std::tuple<T ... > &tuple) const { std::string str{begin(buffer), end(buffer)}; from_string(std::move(str), tuple, std::make_index_sequence<sizeof ... (T)>{}); } private: template <typename T, std::size_t ... I> std::string to_string(T const &tuple, std::index_sequence<I ... >) const { std::stringstream stream; auto put_item = [&stream] (auto const &i) { if constexpr (std::is_same_v<std::decay_t<decltype(i)>, std::string>) stream << std::quoted(i) << ' '; else stream << i << ' '; }; (put_item(std::get<I>(tuple)), ... ); return std::move(stream.str()); } template <typename T, std::size_t ... I> void from_string(std::string str, T &tuple, std::index_sequence<I ... >) const { std::istringstream stream{std::move(str)}; auto get_item = [&stream] (auto &i) { if constexpr (std::is_same_v<std::decay_t<decltype(i)>, std::string>) stream >> std::quoted(i); else stream >> i; }; (get_item(std::get<I>(tuple)), ... ); }
}; } // namespace rpc::packer

И код функции main, демонстрирующий работу сериализатора.

Функция main

int main()
{ try { std::tuple args{10, std::string{"Test string !!!"}, 3.14}; rpc::packer::string_serializer serializer; auto pack = serializer.save(args); std::cout << "Pack data: " << std::string{begin(pack), end(pack)} << std::endl; decltype(args) params; serializer.load(pack, params); // For test { auto pack = serializer.save(params); std::cout << "Deserialized pack: " << std::string{begin(pack), end(pack)} << std::endl; } } catch (std::exception const &e) { std::cerr << "Error: " << e.what() << std::endl; return EXIT_FAILURE; } return EXIT_SUCCESS;
}

Расстановка обещанных акцентов

Первым делом нужно определить буфер, с помощью которого будет производиться весь обмен данными:

namespace rpc::type
{
using buffer = std::vector<char>;
} // namespace rpc::type

Сериализатор имеет методы сохранения кортежа в буфер (save) и загрузки его из буфера (load)

Метод save принимает кортеж и возвращает буфер.

template <typename ... T>
type::buffer save(std::tuple<T ... > const &tuple) const
{ auto str = to_string(tuple, std::make_index_sequence<sizeof ... (T)>{}); return {begin(str), end(str)};
}

Кортеж — шаблон с переменным количеством параметров. Такие шаблоны появились в C++11 и хорошо себя зарекомендовали. Здесь нужно как-то пройти по всем элементам такого шаблона. Вариантов может быть несколько. Воспользуюсь одной из возможностей C++ 14 — последовательностью целых чисел (индексов). В стандартной библиотеке появился тип make_index_sequence, позволяющий получить такую последовательность:

template< class T, T... Ints >
class integer_sequence; template<class T, T N>
using make_integer_sequence = std::integer_sequence<T, /* a sequence 0, 1, 2, ..., N-1 */ >;
template<std::size_t N>
using make_index_sequence = make_integer_sequence<std::size_t, N>;

Аналогичное можно реализовать и на C++11, а после носить за собой из проекта в проект.

Такая последовательность индексов дает возможность «пройти» по кортежу:

template <typename T, std::size_t ... I>
std::string to_string(T const &tuple, std::index_sequence<I ... >) const
{ std::stringstream stream; auto put_item = [&stream] (auto const &i) { if constexpr (std::is_same_v<std::decay_t<decltype(i)>, std::string>) stream << std::quoted(i) << ' '; else stream << i << ' '; }; (put_item(std::get<I>(tuple)), ... ); return std::move(stream.str());
}

Метод to_string использует несколько возможностей последних стандартов C++.

Расстановка обещанных акцентов

Этого часто не хватало, например, при работе с алгоритмами стандартной библиотеки. В C++ 14 появилась возможность использовать auto в качестве параметров для лямбда-функций.

В C++ 17 появилась «свертка», которая позволяет писать такой код, как:

(put_item(std::get<I>(tuple)), ... );

В приведенном фрагменте вызывается лямбда-функция put_item для каждого из элеметов переданного кортежа. При этом гарантирована последовательность не зависящая от платформы и компилятора. Что-то подобное можно было написать и на C++ 11.

template <typename … T>
void unused(T && … ) {}
// ...
unused(put_item(std::get<I>(tuple)) ... );

Но в каком порядке были бы сохранены элементы зависело бы от компилятора.

В стандартной библиотеке C++ 17 появилось много алиасов, например, decay_t, сократившие записи вида:

typename decay<T>::type

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

Стремление к лаконичности дало еще одну интересную конструкцию языка «if constexpr», позволяет избегать написания множества частных специализаций шаблонов.

Многих учили, что switch и аналогичные конструкции — это не очень хорошо с точки зрения масштабируемости кода. Есть интересный момент. А тут «if constexpr»… Возможность компактности не всех оставляет равнодушными к ней. Предпочтительно использовать полиморфизм времени выполнения / времени компиляции и перегрузку с доводами в пользу «правильного выбора». Возможность языка не означает необходимость ее использования.

Для удобной работы со строками, например, при сохранении в поток и чтении из него появилась функция std::quoted. Нужно было написать отдельную сериализацию для строкового типа. Она позволяет экранировать строки и дает возможность сохранения в поток и загружать из него сроки, не думая о разделителе.

Десериализация (load) реализована аналогично. С описанием сериализации пока можно остановиться.

Транспорт

Транспорт прост. Это функция, принимающая и возвращающая буфер.

namespace rpc::type
{
// ...
using executor = std::function<buffer (buffer)>;
} // namespace rpc::type

Формируя подобный объект «исполнитель» с помощью std::bind, лямбда-функций и т. д. можно использовать любую свою реализацию транспорта. Детали реализации транспорта в рамках этого поста рассматриваться не будут. Можно взглянуть на завершенную реализацию RPC, ссылка на которую будет дана в конце.

Клиент

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

Клиент

namespace rpc
{ template <typename TPacker>
class client final
{
private: class result; public: client(type::executor executor) : executor_{executor} { } template <typename ... TArgs> result call(std::string const &func_name, TArgs && ... args) { auto request = std::make_tuple(func_name, std::forward<TArgs>(args) ... ); auto pack = packer_.save(request); auto responce = executor_(std::move(pack)); return {responce}; } private: using packer_type = TPacker; packer_type packer_; type::executor executor_; class result final { public: result(type::buffer buffer) : buffer_{std::move(buffer)} { } template <typename T> auto as() const { std::tuple<std::decay_t<T>> tuple; packer_.load(buffer_, tuple); return std::move(std::get<0>(tuple)); } private: packer_type packer_; type::buffer buffer_; };
}; } // namespace rpc

Клиент реализован в виде шаблонного класса. Параметром шаблона является сериализатор. При необходимости класс можно переделать не в шаблонный и передавать в конструктор объект-реализацию сериализатора.

Исполнитель скрывает под собой реализацию транспорта, и дает возможность в этом месте кода не задумываться о методах обмена данными между процессами. В текущей реализации конструктор класса принимает объект-исполнитель. В тестовом примере реализация транспорта выводит запросы на консоль.

auto executor = [] (rpc::type::buffer buffer)
{ // Print request data std::cout << "Request pack: " << std::string{begin(buffer), end(buffer)} << std::endl; return buffer;
};

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

Метод клиента call:

  • с помощью сериализатора упаковывает имя вызываемого метода и его параметры
  • с помощью объекта-исполнителя отправляет запрос на сервер и принимает ответ
  • передает полученный ответ в класс, извлекающий полученный результат

Базовая реализация клиента готова. Что-то еще осталось. Об этом позже.

Сервер

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

Реализация транспорта — лямбда-функция, передающая буфер между клиентом и сервером. Для простоты демонстрации все в одном процессе.

Клиент-серверное взаимодействие. Тестовый пример

#include <cstdint>
#include <cstdlib>
#include <functional>
#include <iomanip>
#include <iostream>
#include <map>
#include <sstream>
#include <string>
#include <tuple>
#include <vector>
#include <utility> namespace rpc::type
{ using buffer = std::vector<char>;
using executor = std::function<buffer (buffer)>; } // namespace rpc::type namespace rpc::detail
{ template <typename>
struct function_meta; template <typename TRes, typename ... TArgs>
struct function_meta<std::function<TRes (TArgs ... )>>
{ using result_type = std::decay_t<TRes>; using args_type = std::tuple<std::decay_t<TArgs> ... >; using request_type = std::tuple<std::string, std::decay_t<TArgs> ... >;
}; } // namespace rpc::detail namespace rpc::packer
{ class string_serializer final
{
public: template <typename ... T> type::buffer save(std::tuple<T ... > const const &tuple) const { auto str = to_string(tuple, std::make_index_sequence<sizeof ... (T)>{}); return {begin(str), end(str)}; } template <typename ... T> void load(type::buffer const &buffer, std::tuple<T ... > &tuple) const { std::string str{begin(buffer), end(buffer)}; from_string(std::move(str), tuple, std::make_index_sequence<sizeof ... (T)>{}); } private: template <typename T, std::size_t ... I> std::string to_string(T const &tuple, std::index_sequence<I ... >) const { std::stringstream stream; auto put_item = [&stream] (auto const &i) { if constexpr (std::is_same_v<std::decay_t<decltype(i)>, std::string>) stream << std::quoted(i) << ' '; else stream << i << ' '; }; (put_item(std::get<I>(tuple)), ... ); return std::move(stream.str()); } template <typename T, std::size_t ... I> void from_string(std::string str, T &tuple, std::index_sequence<I ... >) const { std::istringstream stream{std::move(str)}; auto get_item = [&stream] (auto &i) { if constexpr (std::is_same_v<std::decay_t<decltype(i)>, std::string>) stream >> std::quoted(i); else stream >> i; }; (get_item(std::get<I>(tuple)), ... ); }
}; } // namespace rpc::packer namespace rpc
{ template <typename TPacker>
class client final
{
private: class result; public: client(type::executor executor) : executor_{executor} { } template <typename ... TArgs> result call(std::string const &func_name, TArgs && ... args) { auto request = std::make_tuple(func_name, std::forward<TArgs>(args) ... ); auto pack = packer_.save(request); auto responce = executor_(std::move(pack)); return {responce}; } private: using packer_type = TPacker; packer_type packer_; type::executor executor_; class result final { public: result(type::buffer buffer) : buffer_{std::move(buffer)} { } template <typename T> auto as() const { std::tuple<std::decay_t<T>> tuple; packer_.load(buffer_, tuple); return std::move(std::get<0>(tuple)); } private: packer_type packer_; type::buffer buffer_; };
}; template <typename TPacker>
class server final
{
public: template <typename ... THandler> server(std::pair<char const *, THandler> const & ... handlers) { auto make_executor = [&packer = packer_] (auto const &handler) { auto executor = [&packer, function = std::function{handler}] (type::buffer buffer) { using meta = detail::function_meta<std::decay_t<decltype(function)>>; typename meta::request_type request; packer.load(buffer, request); auto response = std::apply([&function] (std::string const &, auto && ... args) { return function(std::forward<decltype(args)>(args) ... ); }, std::move(request) ); return packer.save(std::make_tuple(std::move(response))); }; return executor; }; (handlers_.emplace(handlers.first, make_executor(handlers.second)), ... ); } type::buffer execute(type::buffer buffer) { std::tuple<std::string> pack; packer_.load(buffer, pack); auto func_name = std::move(std::get<0>(pack)); auto const iter = handlers_.find(func_name); if (iter == end(handlers_)) throw std::runtime_error{"Function \"" + func_name + "\" not found."}; return iter->second(std::move(buffer)); } private: using packer_type = TPacker; packer_type packer_; using handlers_type = std::map<std::string, type::executor>; handlers_type handlers_;
}; } // namespace rpc int main()
{ try { using packer_type = rpc::packer::string_serializer; rpc::server<packer_type> server{ std::pair{"hello", [] (std::string const &s) { std::cout << "Func: \"hello\". Inpur string: " << s << std::endl; return "Hello " + s + "!"; }}, std::pair{"to_int", [] (std::string const &s) { std::cout << "Func: \"to_int\". Inpur string: " << s << std::endl; return std::stoi(s); }} }; auto executor = [&server] (rpc::type::buffer buffer) { return server.execute(std::move(buffer)); }; rpc::client<packer_type> client{std::move(executor)}; std::cout << client.call("hello", std::string{"world"}).as<std::string>() << std::endl; std::cout << "Convert to int: " << client.call("to_int", std::string{"100500"}).as<int>() << std::endl; } catch (std::exception const &e) { std::cerr << "Error: " << e.what() << std::endl; return EXIT_FAILURE; } return EXIT_SUCCESS;
}

В приведенной реализации класса сервер самое интересное — это его конструктор и метод execute.

Конструктор класса server

template <typename ... THandler>
server(std::pair<char const *, THandler> const & ... handlers)
{ auto make_executor = [&packer = packer_] (auto const &handler) { auto executor = [&packer, function = std::function{handler}] (type::buffer buffer) { using meta = detail::function_meta<std::decay_t<decltype(function)>>; typename meta::request_type request; packer.load(buffer, request); auto response = std::apply([&function] (std::string const &, auto && ... args) { return function(std::forward<decltype(args)>(args) ... ); }, std::move(request) ); return packer.save(std::make_tuple(std::move(response))); }; return executor; }; (handlers_.emplace(handlers.first, make_executor(handlers.second)), ... );
}

Конструктор класса является шаблонным. На вход принимает список пар. Каждая пара — имя метода и обработчик. А так как конструктор является шаблоном с переменным количеством параметров, то при создании объекта server сразу регистрируются все доступные на сервере обработчики. Что даст возможность не делать дополнительных методов регистрации вызываемых на сервере обработчиков. И в свою очередь освобождает от размышлений о том, будет ли объект класса server использоваться в многопоточной среде и нужна ли синхронизация.

Фрагмент конструктора класса server

template <typename ... THandler>
server(std::pair<char const *, THandler> const & ... handlers)
{ // … (handlers_.emplace(handlers.first, make_executor(handlers.second)), ... );
}

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

(handlers_.emplace(handlers.first, make_executor(handlers.second)), ... );

Лямбда-функции, позволяющие использовать auto в качестве параметров дали возможность легко реализовать однотипные обертки над обработчиками. Однотипные обертки регистрируются в карте доступных на сервере методов (std::map). При обработке запросов производится поиск по такой карте, и однотипный вызов найденного обработчика вне зависимости от принимаемых параметров и возвращаемого результата. Появившаяся в стандартной библиотеке функция std::apply вызывает переданную ей функцию с параметрами, переданными в виде кортежа. Функцию std::apply можно реализовать и на C++11. Теперь же она доступна «из коробки» и не надо ее переносить из проекта в проект.

Метод execute

type::buffer execute(type::buffer buffer)
{ std::tuple<std::string> pack; packer_.load(buffer, pack); auto func_name = std::move(std::get<0>(pack)); auto const iter = handlers_.find(func_name); if (iter == end(handlers_)) throw std::runtime_error{"Function \"" + func_name + "\" not found."}; return iter->second(std::move(buffer));
}

Извлекает имя вызываемой функции, производит поиск метода в карте зарегистрированных обработчиков, вызывает обработчик и возвращает результат. Все интересное в обертках подготовленных в конструкторе класса server. Кто-то возможно заметил исключение, и, возможно, возник вопрос: «А исключения как-то обрабатываются?». Да, в полной реализации, которая будет дана ссылкой в конце, маршалинг исключений предусмотрен. Тут же для упрощения материала исключения не передаются между клиентом и сервером.

Взгляните еще раз на функцию

main

int main()
{ try { using packer_type = rpc::packer::string_serializer; rpc::server<packer_type> server{ std::pair{"hello", [] (std::string const &s) { std::cout << "Func: \"hello\". Inpur string: " << s << std::endl; return "Hello " + s + "!"; }}, std::pair{"to_int", [] (std::string const &s) { std::cout << "Func: \"to_int\". Inpur string: " << s << std::endl; return std::stoi(s); }} }; auto executor = [&server] (rpc::type::buffer buffer) { return server.execute(std::move(buffer)); }; rpc::client<packer_type> client{std::move(executor)}; std::cout << client.call("hello", std::string{"world"}).as<std::string>() << std::endl; std::cout << "Convert to int: " << client.call("to_int", std::string{"100500"}).as<int>() << std::endl; } catch (std::exception const &e) { std::cerr << "Error: " << e.what() << std::endl; return EXIT_FAILURE; } return EXIT_SUCCESS;
}

В ней реализовано полноценное клиент-серверное взаимодействия. Чтобы не усложнять материал клиент и сервер работают в один процесс. Заменив реализацию executor, можно использовать нужный транспорт.

В приведенной выше функции main это используется при регистрации обработчиков сервера (std::pair без параметров шаблона) и делает код проще. В стандарте C++ 17 появилась возможность иногда не указывать параметры шаблонов при инстанцировании.

Осталось добавить обещанную возможность передавать пользовательские структуры данных в качестве параметров и возвращаемых результатов. Базовая реализация RPC готова.

Пользовательские структуры данных

Чтобы передать данные через границу процесса их нужно во что-нибудь сериализовать. Например, можно все выводить в стандартный поток. Многое будет поддерживаться «из коробки». Для пользовательских структур данных придется реализовать самостоятельно операторы вывода. Каждой структуре нужен свой оператор вывода. Иногда хочется этого не делать. Чтобы перебрать все поля структуры и вывести каждое поле в поток, нужен какой-то обобщенный метод. В этом могла бы хорошо помочь рефлексия. Ее пока нет в C++. Можно прибегнуть к кодогенерации и использованию смеси из макросов и шаблонов. Но идея была в том, чтобы сделать интерфейс библиотеки на чистом C++.

Поэтому приведенное ниже решение может использоваться с некоторыми ограничениями. Полноценной рефлексии в C++ пока нет.

Часто в диалогах можно встретить много жаргонизмов, поэтому я отказался от каких-либо вариантов названия этой возможности на русском. Решение построено на использовании новой возможности C++ 17 «structured bindings».

Ниже приведено решение, позволяющее перенести в кортеж поля переданной структуры данных.

template <typename T>
auto to_tuple(T &&value)
{ using type = std::decay_t<T>; if constexpr (is_braces_constructible_v<type, dummy_type, dummy_type, dummy_type>) { auto &&[f1, f2, f3] = value; return std::make_tuple(f1, f2, f3); } else if constexpr (is_braces_constructible_v<type, dummy_type, dummy_type>) { auto &&[f1, f2] = value; return std::make_tuple(f1, f2); } else if constexpr (is_braces_constructible_v<type, dummy_type>) { auto &&[f1] = value; return std::make_tuple(f1); } else { return std::make_tuple(); }
}

В Интернете можно найти немало аналогичных решений.

Функция to_tuple принимает пользовательский тип, определяет количество полей, и с помощью structured bindings «перекладывает» поля структуры в кортеж. О многом, что здесь использовано было сказано выше, кроме structured bindings. Так как в C++ рефлексии нет, то полноценное, учитывающее все аспекты типа, решение построить нельзя. А «if constexpr» позволяет выбрать нужную ветвь реализации. Одно из них — тип должен быть без пользовательских конструкторов. Есть ограничения на используемые типы.

Этот тип позволяет определить возможность инициализировать переданную структуру с помощью фигурных скобок и определить количество полей. В to_tuple используется is_braces_constructible_v.

is_braces_constructible_v

struct dummy_type final
{ template <typename T> constexpr operator T () noexcept { return *static_cast<T const *>(nullptr); }
}; template <typename T, typename ... TArgs>
constexpr decltype(void(T{std::declval<TArgs>() ... }), std::declval<std::true_type>())
is_braces_constructible(std::size_t) noexcept; template <typename, typename ... >
constexpr std::false_type is_braces_constructible(...) noexcept; template <typename T, typename ... TArgs>
constexpr bool is_braces_constructible_v = std::decay_t<decltype(is_braces_constructible<T, TArgs ... >(0))>::value;

Приведенная выше функция to_tuple может преобразовывать в кортежи пользовательские структуры данных, содержащие не более трех полей. Чтобы увеличить возможное количество «перекладываемых» полей структуры можно или копировать ветки «if constexpr» с небольшим включением разума, или прибегнуть к использованию не самой простой библиотеки boost.preprocessor. В случае выбора второго варианта код станет трудночитаемым и даст возможность использовать структуры с большим количеством полей.

Реализация to_tuple с помощью boost.preprocessor

template <typename T>
auto to_tuple(T &&value)
{ using type = std::decay_t<T>; #define NANORPC_TO_TUPLE_LIMIT_FIELDS 64 // you can try to use BOOST_PP_LIMIT_REPEAT #define NANORPC_TO_TUPLE_DUMMY_TYPE_N(_, n, data) \ BOOST_PP_COMMA_IF(n) data #define NANORPC_TO_TUPLE_PARAM_N(_, n, data) \ BOOST_PP_COMMA_IF(n) data ## n #define NANORPC_TO_TUPLE_ITEM_N(_, n, __) \ if constexpr (is_braces_constructible_v<type, \ BOOST_PP_REPEAT_FROM_TO(0, BOOST_PP_SUB(NANORPC_TO_TUPLE_LIMIT_FIELDS, n), NANORPC_TO_TUPLE_DUMMY_TYPE_N, dummy_type) \ >) { auto &&[ \ BOOST_PP_REPEAT_FROM_TO(0, BOOST_PP_SUB(NANORPC_TO_TUPLE_LIMIT_FIELDS, n), NANORPC_TO_TUPLE_PARAM_N, f) \ ] = value; return std::make_tuple( \ BOOST_PP_REPEAT_FROM_TO(0, BOOST_PP_SUB(NANORPC_TO_TUPLE_LIMIT_FIELDS, n), NANORPC_TO_TUPLE_PARAM_N, f) \ ); } else #define NANORPC_TO_TUPLE_ITEMS(n) \ BOOST_PP_REPEAT_FROM_TO(0, n, NANORPC_TO_TUPLE_ITEM_N, nil) NANORPC_TO_TUPLE_ITEMS(NANORPC_TO_TUPLE_LIMIT_FIELDS) { return std::make_tuple(); } #undef NANORPC_TO_TUPLE_ITEMS
#undef NANORPC_TO_TUPLE_ITEM_N
#undef NANORPC_TO_TUPLE_PARAM_N
#undef NANORPC_TO_TUPLE_DUMMY_TYPE_N
#undef NANORPC_TO_TUPLE_LIMIT_FIELDS
}

Если Вы когда-либо пробовали сделать что-то подобное boost.bind для C++ 03, где нужно было сделать множество реализаций с разным количеством параметров, то реализация to_tuple с использованием boost.preprocessor не покажется странной или сложной.

И появляется возможность предавать их в качестве параметров и возвращаемых результатов в своем RPC. А если добавить в сериализатор поддержку кортежей, то функция to_tuple даст возможность сериализовать пользовательские структуры данных.

Желание уменьшить количество перегруженных операторов вывода в поток приводит к обобщенному коду, позволяющему одним методом обрабатывать большую часть контейнеров C++ таких, как std::list, std::vector, std::map. Кроме пользовательских структур данных в C++ есть другие встроенные типы, для которых вывод в стандартный поток не реализован. При этом нужно будет как-то косвенно определять свойства типов, подобно тому, как сделано в реализации is_braces_constructible_v. Не забыв про SFINAE и std::enable_if_t можно продолжить расширять сериализатор.

За рамками поста остался маршалинг исключение, транспорт, сериализация stl-контейнеров и многое другое. Дабы сильно не усложнять пост были приведены только общие принципы, на которых мне удалось построить свою RPC библиотеку и решить изначально поставленную для себя же задачу — попробовать новые возможности C++ 14 / 17. А полученная реализация позволяет вызывать удаленные методы по широкораспространенным протоколам HTTP / HTTPS и содержит достаточно подробные примеры использования.

Код библиотеки NanoRPC на GitHub .

Спасибо за внимание!

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть