RPC: una ocasión para probar nuevos en C ++ 14/17

Hace varios años, los desarrolladores de C ++ recibieron el tan esperado estándar C ++ 11, que trajo muchas cosas nuevas. Y me interesaba cambiar rápidamente a su uso en las tareas cotidianas. Vaya a C ++ 14 y 17 esto no fue así. Parecía que no había un conjunto de características que serían de interés. En la primavera, decidí mirar las innovaciones del lenguaje e intentar algo. Para experimentar con innovaciones, tenía que idear una tarea para usted. No tuve que pensar mucho. Se decidió escribir su RPC con estructuras de datos personalizadas como parámetros y sin usar macros y generación de código, todo en C ++. Esto fue posible gracias a las nuevas características del lenguaje.

La idea, implementación, retroalimentación con Reddit, mejoras: todo apareció en la primavera, a principios del verano. Al final, lograron terminar el post en Habr.

¿Has pensado en tu propio RPC? Quizás el material de la publicación lo ayudará a determinar el objetivo, los métodos, los medios y decidir a favor del terminado o implementar algo usted mismo ...

Introduccion


RPC (llamada a procedimiento remoto) no es un tema nuevo. Hay muchas implementaciones en diferentes lenguajes de programación. Las implementaciones utilizan varios formatos de datos y modos de transporte. Todo esto se puede reflejar en algunos puntos:

  • Serialización / Deserialización
  • Transporte
  • Ejecución remota de métodos
  • Resultado devuelto

La implementación está determinada por el objetivo deseado. Por ejemplo, puede establecerse el objetivo de garantizar una alta velocidad de llamada a un método remoto y sacrificar la usabilidad, o viceversa, para proporcionar la máxima comodidad para escribir código, posiblemente perdiendo un poco de rendimiento. Los objetivos y las herramientas son diferentes ... Quería comodidad y un rendimiento aceptable.

Implementación


A continuación se detallan algunos pasos para implementar RPC en C ++ 14/17, y se hace hincapié en algunas de las innovaciones de lenguaje que hicieron que apareciera este material.

El material está destinado a aquellos que por alguna razón están interesados ​​en su RPC y, quizás, hasta ahora, necesitan información adicional. En los comentarios, sería interesante ver una descripción de la experiencia de otros desarrolladores que enfrentan tareas similares.

Serialización


Antes de comenzar a escribir código, formaré una tarea:

  • Todos los parámetros del método y el resultado devuelto se pasan a través de la tupla.
  • Los métodos llamados en sí mismos no están obligados a aceptar y devolver tuplas.
  • El resultado de empacar una tupla debe ser un búfer cuyo formato no sea fijo

El siguiente es un código de serializador de cadena simplificado.

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 { 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 

Y el código de función principal que demuestra el funcionamiento del serializador.

Función principal
 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; } 

Acentos acreditados

En primer lugar, debe determinar el búfer con el que se realizará todo el intercambio de datos:

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

El serializador tiene métodos para guardar una tupla en el búfer (guardar) y cargarla desde el búfer (cargar)

El método de guardar toma una tupla y devuelve un búfer.

 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)}; } 

Una tupla es una plantilla con un número variable de parámetros. Tales patrones aparecieron en C ++ 11 y funcionaron bien. Aquí debe ir de alguna manera a través de todos los elementos de dicha plantilla. Puede haber varias opciones. Usaré una de las características de C ++ 14: una secuencia de enteros (índices). El tipo make_index_sequence ha aparecido en la biblioteca estándar, lo que permite obtener la siguiente secuencia:

 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>; 

Se puede implementar un similar en C ++ 11 y luego llevarlo de un proyecto a otro.

Tal secuencia de índices hace posible "atravesar" la tupla:

 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()); } 

El método to_string utiliza varias características de los últimos estándares de C ++.

Acentos acreditados

En C ++ 14, se hizo posible usar auto como parámetros para las funciones lambda. Esto a menudo no era suficiente, por ejemplo, cuando se trabaja con los algoritmos de la biblioteca estándar.

Apareció una convolución en C ++ 17, que le permite escribir código como:

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

En el fragmento dado, se llama a la función put_item lambda para cada uno de los elementos de la tupla transferida. Esto garantiza una secuencia independiente de la plataforma y el compilador. Algo similar podría escribirse en C ++ 11.

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

Pero en qué orden se almacenarían los elementos dependería del compilador.

Muchos alias aparecieron en la biblioteca estándar de C ++ 17, por ejemplo, decay_t, lo que redujo los registros de la forma:

 typename decay<T>::type 

El deseo de escribir construcciones más cortas tiene un lugar para estar. El diseño de la plantilla, donde se encuentran un par de nombres de tipos y plantillas en una línea, separados por dos puntos y corchetes, parece espeluznante. ¿Cómo puedes asustar a algunos de tus colegas? En el futuro, prometen reducir la cantidad de lugares donde necesita escribir la plantilla, typename.

El deseo de concisión dio otra construcción interesante del lenguaje "si constexpr", evita escribir muchas especializaciones privadas de plantillas.

Hay un punto interesante. A muchos se les ha enseñado que las construcciones de conmutadores y similares no son muy buenas en términos de escalabilidad de código. Es preferible utilizar el polimorfismo en tiempo de ejecución / tiempo de compilación y la sobrecarga con argumentos a favor de la "elección correcta". Y luego "si constexpr" ... La posibilidad de compacidad no deja a todos indiferentes. La posibilidad del lenguaje no significa la necesidad de usarlo.

Era necesario escribir una serialización separada para el tipo de cadena. Para un trabajo conveniente con cadenas, por ejemplo, al guardar en un flujo y leerlo, apareció la función std :: quoted. Le permite filtrar cadenas y hace posible guardar en una secuencia y cargar fechas desde ella sin pensar en el delimitador.

Puede detenerse con la descripción de la serialización por ahora. La deserialización (carga) se implementa de manera similar.

Transporte


El transporte es simple. Esta es una función que recibe y devuelve un búfer.

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

Al formar tal objeto "ejecutor" usando std :: bind, funciones lambda, etc., puede usar cualquiera de sus implementaciones de transporte. No se considerarán los detalles de la implementación del transporte dentro de esta publicación. Puede echar un vistazo a la implementación de RPC completada, un enlace al que se proporcionará al final.

Cliente


A continuación se muestra un código de cliente de prueba. El cliente genera solicitudes y las envía al servidor, teniendo en cuenta el transporte seleccionado. En el código de prueba a continuación, todas las solicitudes de los clientes se muestran en la consola. Y en el siguiente paso de implementación, el cliente ya se comunicará directamente con el servidor.

Cliente
 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 

El cliente se implementa como una clase de plantilla. El parámetro de plantilla es un serializador. Si es necesario, la clase se puede rehacer no en la plantilla uno y pasar al constructor un objeto que implemente el serializador.

En la implementación actual, el constructor de la clase acepta un objeto en ejecución. El contratista oculta la implementación del transporte por sí mismo y hace posible en este punto del código no pensar en métodos para intercambiar datos entre procesos. En el caso de prueba, la implementación de transporte muestra solicitudes a la consola.

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

El código personalizado aún no ha tratado de aprovechar el resultado del trabajo del cliente, ya que no hay de dónde obtenerlo.

Método de llamada del cliente:

  • El uso del serializador incluye el nombre del método llamado y sus parámetros.
  • El uso del objeto en ejecución envía una solicitud al servidor y recibe una respuesta
  • pasa la respuesta recibida a una clase que recupera el resultado recibido

La implementación básica del cliente está lista. Algo más queda. Más sobre esto más tarde.

Servidor


Antes de comenzar a considerar los detalles de implementación del lado del servidor, sugiero una mirada rápida y diagonal al ejemplo completo de interacción cliente-servidor.

Por simplicidad, la demostración es todo en un proceso. La implementación de transporte es una función lambda que pasa un búfer entre el cliente y el servidor.

Interacción cliente-servidor. Caso de prueba
 #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; } 

En la implementación anterior de la clase de servidor, lo más interesante es su constructor y el método de ejecución.

Constructor de clase de servidor

 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)), ... ); } 

El constructor de la clase es repetitivo. En la entrada toma una lista de pares. Cada par es un nombre de método y un controlador. Y dado que el constructor es una plantilla con un número variable de parámetros, al crear el objeto del servidor, todos los controladores disponibles en el servidor se registran de inmediato. Eso permitirá no realizar métodos de registro adicionales llamados en los controladores del servidor. Y, a su vez, lo libera a uno de pensar si el objeto de clase de servidor se usará en un entorno multiproceso y si se necesita sincronización.

Un fragmento del constructor de la clase del servidor.

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

Pone muchos manejadores heterogéneos pasados ​​en el mapa de funciones del mismo tipo. Para esto, también se usa la convolución, lo que facilita poner en el mapa std :: el conjunto completo de controladores pasados ​​en una línea sin bucles y algoritmos

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

Las funciones de Lambda que permiten usar auto como parámetros facilitaron la implementación del mismo tipo de envoltorio sobre los controladores. Las envolturas del mismo tipo se registran en el mapa de métodos disponibles en el servidor (std :: map). Cuando se procesan las solicitudes, se realiza una búsqueda en dicha tarjeta y el mismo controlador llama al controlador encontrado, independientemente de los parámetros recibidos y el resultado devuelto. La función std :: apply que apareció en la biblioteca estándar llama a la función que se le pasa con los parámetros pasados ​​como una tupla. La función std :: apply también se puede implementar en C ++ 11. Ahora está disponible "fuera de la caja" y no hay necesidad de transferirlo de un proyecto a otro.

Método de ejecución

 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)); } 

Recupera el nombre de la función llamada, busca el método en el mapa de controladores registrados, llama al controlador y devuelve el resultado. Todo interesante en los wrappers preparados en el constructor de la clase del servidor. Es posible que alguien haya notado la excepción, y tal vez surgió la pregunta: "¿Las excepciones se manejan de alguna manera?" Sí, en la implementación completa, que se proporcionará como referencia al final, se proporciona la clasificación de excepciones. Allí mismo, para simplificar el material, no se pasan excepciones entre el cliente y el servidor.

Eche otro vistazo a la función.

principal
 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; } 

Implementa una interacción cliente-servidor completa. Para no complicar el material, el cliente y el servidor trabajan en un solo proceso. Reemplazando la implementación del ejecutor, puede utilizar el transporte necesario.

En el estándar C ++ 17, a veces es posible no especificar parámetros de plantilla en la instanciación. En la función principal anterior, esto se utiliza al registrar controladores de servidor (std :: pair sin parámetros de plantilla) y simplifica el código.

La implementación básica de RPC está lista. Queda por agregar la capacidad prometida de pasar estructuras de datos personalizadas como parámetros y devolver resultados.

Estructuras de datos personalizados


Para transferir datos a través del límite del proceso, deben ser serializados en algo. Por ejemplo, puede enviar todo a una secuencia estándar. Mucho será compatible fuera de la caja. Para estructuras de datos personalizadas, deberá implementar los operadores de salida usted mismo. Cada estructura necesita su propio operador de salida. A veces no quieres hacer esto. Para ordenar todos los campos de la estructura y enviar cada campo a la secuencia, necesita algún método generalizado. La reflexión podría ayudar bien en esto. Todavía no está en C ++. Puede recurrir a la generación de código y al uso de una combinación de macros y plantillas. Pero la idea era hacer la interfaz de la biblioteca en C ++ puro.

No hay una reflexión completa en C ++ todavía. Por lo tanto, la solución a continuación se puede usar con algunas limitaciones.

La solución se basa en el uso de la nueva función de "enlaces estructurados" de C ++ 17. A menudo, en los cuadros de diálogo puede encontrar mucha jerga, por lo que rechacé cualquier opción para el nombre de esta función en ruso.

A continuación se muestra una solución que le permite transferir los campos de la estructura de datos transferidos a la tupla.

 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(); } } 

En Internet puede encontrar muchas soluciones similares.

Mucho de lo que se usó aquí se dijo anteriormente, excepto los enlaces estructurados. La función to_tuple acepta un tipo personalizado, determina el número de campos y, con la ayuda de enlaces estructurados, "transfiere" los campos de estructura a una tupla. Y "if constexpr" le permite seleccionar la rama de implementación deseada. Como no hay reflejo en C ++, no se puede construir una solución completa que tenga en cuenta todos los aspectos del tipo. Existen restricciones sobre los tipos utilizados. Uno de ellos: el tipo debe ser sin constructores personalizados.

To_tuple usa is_braces_constructible_v. Este tipo le permite determinar la capacidad de inicializar la estructura transferida utilizando llaves y determinar el número de campos.

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; 

La función to_tuple anterior puede transformar estructuras de datos de usuario que contienen no más de tres campos en tuplas. Para aumentar el número posible de campos "desplazados" de la estructura, puede copiar las ramas "if constexpr" con una pequeña inclusión de la mente, o recurrir a la biblioteca no más simple de boost.preprocessor. Si selecciona la segunda opción, el código será difícil de leer y permitirá usar estructuras con una gran cantidad de campos.

Implementando to_tuple con 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 } 

Si alguna vez ha intentado hacer algo como boost.bind para C ++ 03, donde tuvo que hacer muchas implementaciones con un número diferente de parámetros, la implementación de to_tuple usando boost.preprocessor no parece extraño o complicado.

Y si se agrega soporte de tupla al serializador, la función to_tuple permitirá la serialización de las estructuras de datos del usuario. Y se hace posible traicionarlos como parámetros y devolver resultados en su RPC.

Además de las estructuras de datos definidas por el usuario, C ++ tiene otros tipos integrados para los que no se implementa la salida a la secuencia estándar. El deseo de reducir el número de operadores de salida sobrecargados en el flujo conduce a un código generalizado que permite que un método procese la mayoría de los contenedores de C ++, como std :: list, std :: vector, std :: map. Sin olvidarse de SFINAE y std :: enable_if_t, puede continuar extendiendo el serializador. En este caso, será necesario determinar indirectamente de alguna manera las propiedades de los tipos, similar a lo que se hace en la implementación de is_braces_constructible_v.

Conclusión


Fuera del alcance de la publicación está la excepción de clasificación, transporte, serialización de contenedores stl y mucho más. Para no complicar mucho la publicación, solo se dieron principios generales sobre los cuales pude construir mi biblioteca RPC y resolver el conjunto de tareas original por mí mismo: probar nuevas características de C ++ 14/17. Y la implementación resultante le permite llamar a métodos remotos utilizando el extendido HTTP / HTTPS y contiene ejemplos de uso bastante detallados. Código de la

biblioteca NanoRPC en GitHub .

Gracias por su atencion!

Source: https://habr.com/ru/post/es421001/


All Articles