RPC is a reason to try something new in C ++ 14/17

    A few years ago, C ++ developers got the long-awaited C ++ 11 standard, which brought a lot of new things. And I had an interest to move quickly to its use in everyday tasks. Go to C ++ 14 and 17 was not like this. It seemed that there is no feature set that would interest. In the spring I decided to look at the innovations of the language and try something. To experiment with innovations, you had to invent a task for yourself. I did not have to think long. It was decided to write my own RPC with user data structures as parameters and without using macros and code generation - all in C ++. This was possible thanks to the new features of the language.

    The idea, the implementation, feedback with Reddit, improvements - everything appeared in the spring, early summer. By the end it was possible to add a post on Habr.

    Are you thinking about your own RPC? Perhaps, the material of the post will help you to decide on the purpose, methods, means and decide for the benefit of the finished one or implement something yourself ...

    Introduction


    RPC (remote procedure call) is not a new topic. There are many implementations in different programming languages. Implementations use different data formats and modes of transport. All this can be reflected in several points:

    • Serialization / deserialization
    • Transport
    • Run remote method
    • Return result

    Implementation is determined by the desired goal. For example, you can set a goal to provide a high speed call to a remote method and sacrifice usability, or vice versa to ensure maximum comfort in writing code, perhaps a little lost in performance. The goals and tools are different ... I wanted comfort and acceptable performance.

    Implementation


    Below are a few steps of implementing RPC in C ++ 14/17, and emphasis has been placed on some of the innovations in the language that have caused this material to appear.

    The material is intended for those who for some reason are interested in their RPC, and, perhaps for now, need additional information. In the comments it would be interesting to see a description of the experience of other developers who are faced with similar tasks.

    Serialization


    Before you start writing code, I will create a task:

    • All method parameters and the return result are passed through the tuple.
    • Called methods themselves are not required to accept and return tuples.
    • The result of packing a tuple must be a buffer whose format is not fixed.

    Below is the simplified string serializer code.

    string_serializer
    namespace rpc::type
    {
    using buffer = std::vector<char>;
    }   // namespace rpc::typenamespace rpc::packer
    {
    classstring_serializerfinal
    {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>
        voidload(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::stringto_string(T const &tuple, std::index_sequence<I ... >)const{
            std::stringstream stream;
            auto put_item = [&stream] (autoconst &i)
            {
                ifconstexpr (std::is_same_v<std::decay_t<decltype(i)>, std::string>)
                    stream << std::quoted(i) << ' ';
                else
                    stream << i << ' ';
            };
            (put_item(std::get<I>(tuple)), ... );
            returnstd::move(stream.str());
        }
        template <typename T, std::size_t ... I>
        voidfrom_string(std::string str, T &tuple, std::index_sequence<I ... >)const{
            std::istringstream stream{std::move(str)};
            auto get_item = [&stream] (auto &i)
            {
                ifconstexpr (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

    And the main function code, which demonstrates the work of the serializer.

    Main function
    intmain(){
        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;
    }
    

    The arrangement of the promised accents

    First of all, you need to determine the buffer with which the entire data exchange will be performed:

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

    The serializer has methods for saving a tuple to the buffer (save) and loading it from the buffer (load)

    The save method takes a tuple and returns a buffer.

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

    A tuple is a template with a variable number of parameters. Such templates appeared in C ++ 11 and have proven themselves well. Here you need to somehow go through all the elements of such a template. There may be several options. I will use one of the features of C ++ 14 - a sequence of integers (indices). The standard library now has the type make_index_sequence, which allows to get the following sequence:

    template< classT, T... Ints >
    classinteger_sequence;template<classT, TN>
    usingmake_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>;
    

    Similar can be implemented in C ++ 11, and then carried along from project to project.

    Such a sequence of indices makes it possible to “pass” through a tuple:

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

    The to_string method uses several features of the latest C ++ standards.

    Placement of promised accents

    In C ++ 14, it became possible to use auto as parameters for lambda functions. This was often lacking, for example, when working with standard library algorithms.

    In C ++ 17, “convolution” appeared , which allows you to write code such as:

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

    In the above fragment, the lambda put_item function is called for each of the elements of the passed tuple. At the same time, a sequence independent of the platform and the compiler is guaranteed. Something similar could be written in C ++ 11.

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

    But in what order the elements would be stored would depend on the compiler.

    In the standard library C ++ 17, many aliases appeared, for example, decay_t, which reduced the entries like:

    typename decay<T>::type
    

    The desire to write shorter constructions takes place. The template construction, where in one line there is a couple of typename and template, separated by colons and angle brackets, looks scary. What can scare some of their colleagues? In the future, they promise to reduce the number of places where it is necessary to write a template, typename.

    The desire for conciseness gave another interesting design language "if constexpr", allows you to avoid writing a lot of private specializations of templates.

    There is an interesting point. Many have learned that switch and similar constructions are not very good in terms of code scalability. It is preferable to use compile time / compile time polymorphism and overload with arguments in favor of the “right choice”. And then “if constexpr” ... The possibility of compactness does not leave everyone indifferent to it. The possibility of language does not mean the need to use it.

    It was necessary to write a separate serialization for the string type. For convenient work with strings, for example, std :: quoted. Has appeared in the stream while reading and reading from it. It allows you to screen lines and allows you to save to the stream and load dates from it, without thinking about the separator.

    With a description of the serialization, you can stop. Deserialization (load) is implemented similarly.

    Transport


    Transportation is simple. This is the function that receives and returns the buffer.

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

    Forming a similar “executor” object using std :: bind, lambda functions, etc., you can use any of your transport implementations. Details of the implementation of transport in this post will not be considered. You can take a look at the completed RPC implementation, which will be referenced at the end.

    Customer


    Below is the client test code. The client creates requests and sends them to the server based on the selected transport. In the following test code, all client requests are displayed on the console. And in the next step of the implementation, the client will communicate directly with the server.

    Customer
    namespace rpc
    {
    template <typename TPacker>
    classclientfinal
    {private:
        classresult;public:
        client(type::executor executor)
            : executor_{executor}
        {
        }
        template <typename ... TArgs>
        result call(std::stringconst &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_;
        classresultfinal
        {public:
            result(type::buffer buffer)
                : buffer_{std::move(buffer)}
            {
            }
            template <typename T>
            autoas()const{
                std::tuple<std::decay_t<T>> tuple;
                packer_.load(buffer_, tuple);
                returnstd::move(std::get<0>(tuple));
            }
        private:
            packer_type packer_;
            type::buffer buffer_;
        };
    };
    }   // namespace rpc

    The client is implemented as a template class. The template parameter is a serializer. If necessary, the class can be converted into a non-template one and passed into the constructor an implementation object of the serializer.

    In the current implementation, the class constructor accepts the executable object. The executor hides the implementation of the transport, and makes it possible in this place of the code not to think about the methods of data exchange between processes. In the test case, the implementation of the transport displays requests to the console.

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

    The user code is not trying to use the result of the client’s work yet, as it’s not from where to get it.

    Customer call method:

    • using the serializer packs the name of the called method and its parameters
    • using an executing object, sends a request to the server and accepts the response
    • sends the received response to the class that retrieves the result

    The basic client implementation is ready. Something else remains. More on that later.

    Server


    Before proceeding to the consideration of the details of the implementation of the server side, I propose a quick, diagonal look at the completed example of client-server interaction.

    For ease of demonstration all in one process. Transport implementation is a lambda function that transfers a buffer between the client and the server.

    Client-server interaction. Test case
    #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::typenamespace rpc::detail
    {
    template <typename>
    structfunction_meta;template <typename TRes, typename ... TArgs>
    structfunction_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::detailnamespace rpc::packer
    {
    classstring_serializerfinal
    {public:
        template <typename ... T>
        type::buffer save(std::tuple<T ... > constconst &tuple)const{
            auto str = to_string(tuple, std::make_index_sequence<sizeof ... (T)>{});
            return {begin(str), end(str)};
        }
        template <typename ... T>
        voidload(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::stringto_string(T const &tuple, std::index_sequence<I ... >)const{
            std::stringstream stream;
            auto put_item = [&stream] (autoconst &i)
            {
                ifconstexpr (std::is_same_v<std::decay_t<decltype(i)>, std::string>)
                    stream << std::quoted(i) << ' ';
                else
                    stream << i << ' ';
            };
            (put_item(std::get<I>(tuple)), ... );
            returnstd::move(stream.str());
        }
        template <typename T, std::size_t ... I>
        voidfrom_string(std::string str, T &tuple, std::index_sequence<I ... >)const{
            std::istringstream stream{std::move(str)};
            auto get_item = [&stream] (auto &i)
            {
                ifconstexpr (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::packernamespace rpc
    {
    template <typename TPacker>
    classclientfinal
    {private:
        classresult;public:
        client(type::executor executor)
            : executor_{executor}
        {
        }
        template <typename ... TArgs>
        result call(std::stringconst &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_;
        classresultfinal
        {public:
            result(type::buffer buffer)
                : buffer_{std::move(buffer)}
            {
            }
            template <typename T>
            autoas()const{
                std::tuple<std::decay_t<T>> tuple;
                packer_.load(buffer_, tuple);
                returnstd::move(std::get<0>(tuple));
            }
        private:
            packer_type packer_;
            type::buffer buffer_;
        };
    };
    template <typename TPacker>
    classserverfinal
    {public:
        template <typename ... THandler>
        server(std::pair<charconst *, THandler> const & ... handlers)
        {
            auto make_executor = [&packer = packer_] (autoconst &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::stringconst &, 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));
            autoconst iter = handlers_.find(func_name);
            if (iter == end(handlers_))
                throwstd::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 rpcintmain(){
        try
        {
            using packer_type = rpc::packer::string_serializer;
            rpc::server<packer_type> server{
                std::pair{"hello",
                    [] (std::stringconst &s)
                    {
                        std::cout << "Func: \"hello\". Inpur string: " << s << std::endl;
                        return"Hello " + s + "!";
                    }},
                std::pair{"to_int",
                    [] (std::stringconst &s)
                    {
                        std::cout << "Func: \"to_int\". Inpur string: " << s << std::endl;
                        returnstd::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;
    }
    

    In the above implementation of the class, the most interesting thing about the server is its constructor and the execute method.

    Constructor class server

    template <typename ... THandler>
    server(std::pair<charconst *, THandler> const & ... handlers)
    {
        auto make_executor = [&packer = packer_] (autoconst &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::stringconst &, 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)), ... );
    }
    

    The class constructor is template. At the entrance takes a list of pairs. Each pair is a method name and a handler. And since the constructor is a template with a variable number of parameters, when a server object is created, all the handlers available on the server are immediately registered. This will make it possible not to make additional registration methods for handlers called on the server. And in turn, it frees you from thinking about whether the object of the class server will be used in a multithreaded environment and whether synchronization is necessary.

    Fragment of the server class constructor

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

    Places a lot of passed handlers of different types into a map of functions of the same type. For this, a convolution is also used, which makes it easy to put into std :: map the entire set of passed handlers in one line without cycles and algorithms.

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

    Lambda functions that allow the use of auto as parameters have made it possible to easily implement the same type of wrapper over handlers. Wrappers of the same type are registered in the methods available on the server (std :: map). When processing requests, a search is performed on such a map, and a similar call to the found handler, regardless of the parameters received and the returned result. The std :: apply function that appeared in the standard library calls the function passed to it with the parameters passed as a tuple. The std :: apply function can also be implemented in C ++ 11. Now it is available “out of the box” and it is not necessary to transfer it from project to project.

    Execute method

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

    Retrieves the name of the function being called, searches the method in the map of the registered handlers, calls the handler and returns the result. Everything is interesting in wrappers prepared in the constructor of the server class. Someone may have noticed an exception, and perhaps the question arose: “Are the exceptions somehow handled?”. Yes, in full implementation, which will be given by reference at the end, the marshalling of exceptions is provided. Immediately to simplify the material, exceptions are not passed between the client and the server.

    Take another look at the function.

    main
    intmain(){
        try
        {
            using packer_type = rpc::packer::string_serializer;
            rpc::server<packer_type> server{
                std::pair{"hello",
                    [] (std::stringconst &s)
                    {
                        std::cout << "Func: \"hello\". Inpur string: " << s << std::endl;
                        return"Hello " + s + "!";
                    }},
                std::pair{"to_int",
                    [] (std::stringconst &s)
                    {
                        std::cout << "Func: \"to_int\". Inpur string: " << s << std::endl;
                        returnstd::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;
    }
    

    It implements a full-fledged client-server interaction. In order not to complicate the material, the client and server work in a single process. Replacing the implementation executor, you can use the desired transport.

    In the standard C ++ 17, it is sometimes possible not to specify template parameters when instantiating. In the above main function, this is used when registering server handlers (std :: pair without template parameters) and makes the code easier.

    The basic RPC implementation is ready. It remains to add the promised ability to pass custom data structures as parameters and returned results.

    Custom data structures


    To transfer data across the process boundary, they need to be serialized into something. For example, everything can be output to the standard stream. Much will be supported out of the box. For user data structures, you will have to implement output operators yourself. Each structure needs its own output operator. Sometimes you want not to do it. To iterate through all the fields of a structure and output each field to a stream, we need some generalized method. This could well help reflexion. It is not yet in C ++. You can resort to code generation and use a mixture of macros and templates. But the idea was to make the library interface in pure C ++.

    There is no complete reflection in C ++ yet. Therefore, the solution below can be used with some limitations.

    The solution is based on the use of the new C ++ 17 “structured bindings” feature. Often in conversations you can find a lot of jargon, so I refused any options for naming this feature in Russian.

    Below is a solution that allows you to transfer the fields of the transmitted data structure to a tuple.

    template <typename T>
    autoto_tuple(T &&value){
        using type = std::decay_t<T>;
        ifconstexpr(is_braces_constructible_v<type, dummy_type, dummy_type, dummy_type>){
            auto &&[f1, f2, f3] = value;
            returnstd::make_tuple(f1, f2, f3);
        }
        elseifconstexpr (is_braces_constructible_v<type, dummy_type, dummy_type>)
        {
            auto &&[f1, f2] = value;
            returnstd::make_tuple(f1, f2);
        }
        elseifconstexpr (is_braces_constructible_v<type, dummy_type>)
        {
            auto &&[f1] = value;
            returnstd::make_tuple(f1);
        }
        else
        {
            returnstd::make_tuple();
        }
    }
    

    On the Internet you can find many similar solutions.

    Much of what was used here was mentioned above, except for the structured bindings. The to_tuple function takes a custom type, determines the number of fields, and with the help of structured bindings, “shifts” the structure fields into a tuple. And “if constexpr” allows you to select the desired implementation branch. Since there is no reflection in C ++, a full-fledged solution that takes into account all aspects of the type cannot be built. There are restrictions on the types used. One of them is the type must be without custom constructors.

    The to_tuple uses is_braces_constructible_v. This type allows you to determine the ability to initialize the passed structure using curly brackets and determine the number of fields.

    is_braces_constructible_v
    structdummy_typefinal
    {template <typename T>
        constexproperatorT()noexcept{
            return *static_cast<T const *>(nullptr);
        }
    };
    template <typename T, typename ... TArgs>
    constexprdecltype(void(T{std::declval<TArgs>() ... }), std::declval<std::true_type>())is_braces_constructible(std::size_t)noexcept;
    template <typename, typename ... >
    constexprstd::false_type is_braces_constructible(...)noexcept;
    template <typename T, typename ... TArgs>
    constexprbool is_braces_constructible_v = std::decay_t<decltype(is_braces_constructible<T, TArgs ... >(0))>::value;
    

    The above to_tuple function can convert custom data structures containing up to three fields to tuples. To increase the possible number of “shifted” fields of the structure, you can either copy the “if constexpr” branches with a little mind switching on, or resort to using the not so simple library boost.preprocessor. If the second option is chosen, the code will become difficult to read and will provide an opportunity to use structures with a large number of fields.

    Implementing to_tuple with boost.preprocessor
    template <typename T>
    autoto_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)
        {
            returnstd::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
    }
    

    If you have ever tried to do something like boost.bind for C ++ 03, where you had to do a lot of implementations with a different number of parameters, then implementing to_tuple using boost.preprocessor does not seem strange or difficult.

    And if you add support for tuples to the serializer, then the function to_tuple will give the opportunity to serialize user data structures. And it becomes possible to deliver them as parameters and returned results in your RPC.

    In addition to custom data structures, C ++ has other built-in types for which output to the standard stream is not implemented. The desire to reduce the number of overloaded output statements to a stream leads to generalized code, which allows one method to handle most of the C ++ containers such as std :: list, std :: vector, std :: map. Not forgetting about SFINAE and std :: enable_if_t you can continue to expand the serializer. In this case, it will be necessary to somehow indirectly determine the properties of types, just as is done in the implementation of is_braces_constructible_v.

    Conclusion


    Outside the post was marshaling the exception, transport, serialization of stl-containers and much more. In order not to complicate the post, only general principles were given, on which I managed to build my RPC library and solve the task originally posed for myself - try new C ++ 14/17 capabilities. contains fairly detailed usage examples. NanoRPC

    library code on github .

    Thanks for attention!

    Also popular now: