
Il y a plusieurs années, les développeurs C ++ ont reçu la norme C ++ 11 tant attendue, qui a apporté beaucoup de nouvelles choses. Et j'avais intérêt à passer rapidement à son utilisation dans les tâches quotidiennes. Allez en C ++ 14 et 17 ce n'était pas le cas. Il semblait qu'il n'existait aucun ensemble de caractéristiques susceptibles d'être intéressantes. Au printemps, j'ai décidé de regarder les innovations de la langue et d'essayer quelque chose. Pour expérimenter les innovations, vous deviez trouver une tâche pour vous-même. Je n'ai pas eu à réfléchir longtemps. Il a été décidé d'écrire votre RPC avec des structures de données personnalisées en tant que paramètres et sans utiliser de macros et de génération de code - le tout en C ++. Cela a été possible grâce aux nouvelles fonctionnalités du langage.
L'idée, l'implémentation, le feedback avec Reddit, les améliorations - tout est apparu au printemps, au début de l'été. À la fin, ils ont réussi à terminer le poste sur Habr.
Avez-vous pensé à votre propre RPC? Peut-être que le matériel du message vous aidera à déterminer l'objectif, les méthodes, les moyens et à décider en faveur de celui qui est terminé ou à mettre en œuvre quelque chose vous-même ...
Présentation
RPC (appel de procédure distante) n'est pas un nouveau sujet. Il existe de nombreuses implémentations dans différents langages de programmation. Les implémentations utilisent différents formats de données et modes de transport. Tout cela peut se refléter en quelques points:
- Sérialisation / désérialisation
- Le transport
- Exécution de méthode à distance
- Résultat de retour
La mise en œuvre est déterminée par l'objectif souhaité. Par exemple, vous pouvez vous fixer l'objectif d'assurer une vitesse élevée d'appel d'une méthode à distance et de sacrifier la convivialité, ou vice versa, pour fournir un confort maximum pour l'écriture de code, en perdant éventuellement un peu de performance. Les objectifs et les outils sont différents ... Je voulais du confort et des performances acceptables.
Implémentation
Voici quelques étapes pour implémenter RPC en C ++ 14/17, et l'accent est mis sur certaines des innovations de langage qui ont fait apparaître ce matériel.
Le matériel est destiné à ceux qui, pour une raison quelconque, sont intéressés par leur RPC et, peut-être, jusqu'à présent, ont besoin d'informations supplémentaires. Dans les commentaires, il serait intéressant de voir une description de l'expérience d'autres développeurs confrontés à des tâches similaires.
Sérialisation
Avant de commencer à écrire du code, je vais former une tâche:
- Tous les paramètres de méthode et le résultat renvoyé sont transmis via le tuple.
- Les méthodes appelées elles-mêmes ne sont pas obligées d'accepter et de renvoyer des tuples.
- Le résultat de l'empaquetage d'un tuple devrait être un tampon dont le format n'est pas fixe
Voici un code de sérialiseur de chaîne simplifié.
string_serializernamespace rpc::type { using buffer = std::vector<char>; }
Et le code de fonction principal démontrant le fonctionnement du sérialiseur.
Fonction principale 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);
Accents accréditésTout d'abord, vous devez déterminer le tampon avec lequel l'ensemble de l'échange de données sera effectué:
namespace rpc::type { using buffer = std::vector<char>; }
Le sérialiseur dispose de méthodes pour enregistrer un tuple dans le tampon (enregistrer) et le charger à partir du tampon (charger)
La méthode save prend un tuple et retourne un tampon.
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)}; }
Un tuple est un modèle avec un nombre variable de paramètres. Ces modèles sont apparus en C ++ 11 et ont bien fonctionné. Ici, vous devez en quelque sorte parcourir tous les éléments d'un tel modèle. Il peut y avoir plusieurs options. J'utiliserai l'une des fonctionnalités de C ++ 14 - une séquence d'entiers (indices). Le type make_index_sequence est apparu dans la bibliothèque standard, ce qui permet d'obtenir la séquence suivante:
template< class T, T... Ints > class integer_sequence; template<class T, T N> using make_integer_sequence = std::integer_sequence<T, >; template<std::size_t N> using make_index_sequence = make_integer_sequence<std::size_t, N>;
Un similaire peut être implémenté en C ++ 11, puis le transporter de projet en projet.
Une telle séquence d'indices permet de "parcourir" le tuple:
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()); }
La méthode to_string utilise plusieurs fonctionnalités des dernières normes C ++.
Accents accréditésEn C ++ 14, il est devenu possible d'utiliser auto comme paramètres pour les fonctions lambda. Cela n'était souvent pas suffisant, par exemple, lorsque l'on travaillait avec les algorithmes de la bibliothèque standard.
Une
convolution est apparue en C ++ 17, qui vous permet d'écrire du code tel que:
(put_item(std::get<I>(tuple)), ... );
Dans le fragment donné, la fonction lambda put_item est appelée pour chacun des éléments du tuple transféré. Cela garantit une séquence indépendante de la plateforme et du compilateur. Quelque chose de similaire pourrait être écrit en C ++ 11.
template <typename … T> void unused(T && … ) {}
Mais dans quel ordre les éléments seraient stockés dépendrait du compilateur.
De nombreux alias sont apparus dans la bibliothèque standard C ++ 17, par exemple decay_t, ce qui a réduit les enregistrements du formulaire:
typename decay<T>::type
Le désir d'écrire des constructions plus courtes a sa place. La conception du modèle, où quelques noms de caractères et modèles se trouvent sur une seule ligne, séparés par des deux-points et des crochets, semble effrayante. Comment pouvez-vous effrayer certains de vos collègues. À l'avenir, ils promettent de réduire le nombre d'endroits où vous devez écrire le modèle, le nom de type.
Le désir de concision a donné une autre construction intéressante du langage «si constexpr», évite d'écrire de nombreuses spécialisations privées de modèles.
Il y a un point intéressant. Beaucoup ont appris que le commutateur et les constructions similaires ne sont pas très bons en termes d'évolutivité du code. Il est préférable d'utiliser le polymorphisme d'exécution / compilation et la surcharge avec des arguments en faveur du «bon choix». Et puis «si constexpr» ... La possibilité de compacité ne laisse pas tout le monde indifférent. La possibilité de la langue ne signifie pas la nécessité de l'utiliser.
Il était nécessaire d'écrire une sérialisation distincte pour le type de chaîne. Pour un travail pratique avec des chaînes, par exemple, lors de l'enregistrement dans un flux et de sa lecture, la fonction std :: quoted est apparue. Il vous permet de filtrer les chaînes et permet d'enregistrer dans un flux et de charger des dates à partir de celui-ci sans penser au délimiteur.
Vous pouvez vous arrêter à la description de la sérialisation pour l'instant. La désérialisation (charge) est implémentée de manière similaire.
Le transport
Le transport est simple. Il s'agit d'une fonction qui reçoit et renvoie un tampon.
namespace rpc::type {
En formant un tel «exécuteur» d'objet à l'aide de std :: bind, des fonctions lambda, etc., vous pouvez utiliser n'importe laquelle de vos implémentations de transport. Les détails de la mise en œuvre du transport dans ce poste ne seront pas pris en compte. Vous pouvez jeter un œil à l'implémentation RPC terminée, un lien vers lequel sera donné à la fin.
Client
Voici un code client de test. Le client génère des requêtes et les envoie au serveur, en tenant compte du transport sélectionné. Dans le code de test ci-dessous, toutes les demandes des clients sont affichées sur la console. Et à la prochaine étape de mise en œuvre, le client communiquera déjà directement avec le serveur.
Client 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_; }; }; }
Le client est implémenté en tant que classe de modèle. Le paramètre de modèle est un sérialiseur. Si nécessaire, la classe peut être refaite pas dans celle du modèle et passée au constructeur un objet qui implémente le sérialiseur.
Dans l'implémentation actuelle, le constructeur de classe accepte un objet en cours d'exécution. L'entrepreneur cache sous lui-même la mise en œuvre du transport, et permet à ce stade du code de ne pas penser aux méthodes d'échange de données entre processus. Dans le cas de test, l'implémentation de transport affiche les demandes à la console.
auto executor = [] (rpc::type::buffer buffer) {
Le code personnalisé n'a pas encore essayé de tirer parti du résultat du travail du client, car il n'y a aucun endroit où l'obtenir.
Méthode d'appel client:- l'utilisation du sérialiseur contient le nom de la méthode appelée et ses paramètres
- l'utilisation de l'objet d'exécution envoie une demande au serveur et reçoit une réponse
- transmet la réponse reçue à une classe qui extrait le résultat
L'implémentation client de base est prête. Il reste quelque chose d'autre. Plus d'informations à ce sujet plus tard.
Serveur
Avant de commencer à considérer les détails d'implémentation côté serveur, je suggère un rapide coup d'œil en diagonale sur l'exemple complet d'interaction client-serveur.
Par souci de simplicité, la démonstration se fait en un seul processus. L'implémentation de transport est une fonction lambda qui passe un tampon entre le client et le serveur.
Interaction client-serveur. Cas de test #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; }
Dans l'implémentation ci-dessus de la classe serveur, la chose la plus intéressante est son constructeur et la méthode execute.
Constructeur de classe de serveur 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)), ... ); }
Le constructeur de la classe est passe-partout. Il accepte une liste de paires en entrée Chaque paire est un nom de méthode et un gestionnaire. Et puisque le constructeur est un modèle avec un nombre variable de paramètres, lors de la création de l'objet serveur, tous les gestionnaires disponibles sur le serveur sont enregistrés immédiatement. Cela permettra de ne pas faire de méthodes d'enregistrement supplémentaires appelées sur les gestionnaires de serveur. Et, à son tour, libère quelqu'un de la question de savoir si l'objet de classe de serveur sera utilisé dans un environnement multithread et si la synchronisation est nécessaire.
Un fragment du constructeur de la classe serveur template <typename ... THandler> server(std::pair<char const *, THandler> const & ... handlers) {
Place un grand nombre de gestionnaires hétérogènes passés dans la carte des fonctions du même type. Pour cela, la convolution est également utilisée, ce qui permet de mettre facilement dans le std :: map l'ensemble des gestionnaires passés sur une seule ligne sans boucles ni algorithmes
(handlers_.emplace(handlers.first, make_executor(handlers.second)), ... );
Les fonctions Lambda qui permettent d'utiliser auto comme paramètres ont facilité l'implémentation du même type de wrapper sur les gestionnaires. Les enveloppements du même type sont enregistrés dans la carte des méthodes disponibles sur le serveur (std :: map). Lors du traitement des demandes, une recherche est effectuée sur une telle carte et le même gestionnaire appelle le gestionnaire trouvé, quels que soient les paramètres reçus et le résultat renvoyé. La fonction std :: apply qui est apparue dans la bibliothèque standard appelle la fonction qui lui est passée avec les paramètres passés en tant que tuple. La fonction std :: apply peut également être implémentée en C ++ 11. Maintenant, il est disponible «prêt à l'emploi» et il n'est pas nécessaire de le transférer d'un projet à l'autre.
Exécuter la méthode 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)); }
Récupère le nom de la fonction appelée, recherche la méthode dans la carte des gestionnaires enregistrés, appelle le gestionnaire et renvoie le résultat. Tous intéressants dans les wrappers préparés dans le constructeur de la classe serveur. Quelqu'un a peut-être remarqué l'exception et peut-être la question s'est-elle posée: "Les exceptions sont-elles gérées d'une manière ou d'une autre?" Oui, dans l'implémentation complète, qui sera donnée par référence à la fin, un marshaling d'exception est fourni. Pour simplifier le matériel, aucune exception n'est transmise entre le client et le serveur.
Jetez un autre regard sur la fonction
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; }
Il implémente une interaction client-serveur à part entière. Afin de ne pas compliquer le matériel, le client et le serveur travaillent en un seul processus. En remplaçant l'implémentation de l'exécuteur, vous pouvez utiliser le transport nécessaire.
Dans la norme C ++ 17, il est parfois possible de ne pas spécifier de paramètres de modèle lors de l'instanciation. Dans la fonction principale ci-dessus, ceci est utilisé lors de l'enregistrement des gestionnaires de serveur (std :: pair sans paramètres de modèle) et rend le code plus simple.
L'implémentation RPC de base est prête. Il reste à ajouter la capacité promise de passer des structures de données personnalisées en tant que paramètres et de renvoyer des résultats.
Structures de données personnalisées
Pour transférer des données à travers la frontière du processus, elles doivent être sérialisées en quelque chose. Par exemple, vous pouvez tout exporter vers un flux standard. Beaucoup sera pris en charge hors de la boîte. Pour les structures de données personnalisées, vous devrez implémenter vous-même les opérateurs de sortie. Chaque structure a besoin de son propre opérateur de sortie. Parfois, vous ne voulez pas faire ça. Pour trier tous les champs de la structure et sortir chaque champ dans le flux, vous avez besoin d'une méthode généralisée. La réflexion pourrait bien y aider. Ce n'est pas encore en C ++. Vous pouvez recourir à la génération de code et à l'utilisation d'un mélange de macros et de modèles. Mais l'idée était de faire l'interface de la bibliothèque en C ++ pur.
Il n'y a pas encore de réflexion complète en C ++. Par conséquent, la solution ci-dessous peut être utilisée avec certaines limitations.
La solution est basée sur l'utilisation de la nouvelle fonctionnalité «liaisons structurées» C ++ 17. Souvent, dans les dialogues, vous pouvez trouver beaucoup de jargon, j'ai donc refusé toute option pour le nom de cette fonctionnalité en russe.
Vous trouverez ci-dessous une solution qui vous permet de transférer les champs de la structure de données transférée vers le tuple.
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(); } }
Sur Internet, vous pouvez trouver de nombreuses solutions similaires.
Une grande partie de ce qui a été utilisé ici a été dit ci-dessus, à l'exception des liaisons structurées. La fonction to_tuple accepte un type personnalisé, détermine le nombre de champs et, à l'aide de liaisons structurées, "transfère" les champs de structure à un tuple. Et «si constexpr» vous permet de sélectionner la branche d'implémentation souhaitée. Comme il n'y a pas de réflexion en C ++, une solution complète qui prend en compte tous les aspects du type ne peut pas être construite. Il existe des restrictions sur les types utilisés. L'un d'eux - le type doit être sans constructeur personnalisé.
To_tuple utilise is_braces_constructible_v. Ce type vous permet de déterminer la capacité d'initialiser la structure transférée à l'aide d'accolades et de déterminer le nombre de champs.
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 fonction to_tuple ci-dessus peut transformer des structures de données utilisateur ne contenant pas plus de trois champs en tuples. Pour augmenter le nombre possible de champs «décalés» de la structure, vous pouvez soit copier les branches «if constexpr» avec une petite inclusion de l'esprit, soit recourir à la bibliothèque boost.preprocessor la plus simple. Si vous sélectionnez la deuxième option, le code deviendra difficile à lire et permettra d'utiliser des structures avec un grand nombre de champs.
Implémentation de to_tuple avec boost.preprocessor template <typename T> auto to_tuple(T &&value) { using type = std::decay_t<T>; #define NANORPC_TO_TUPLE_LIMIT_FIELDS 64
Si vous avez déjà essayé de faire quelque chose comme boost.bind pour C ++ 03, où vous avez dû faire de nombreuses implémentations avec un nombre différent de paramètres, alors l'implémentation de to_tuple en utilisant boost.preprocessor ne semble ni étrange ni compliquée.
Et si la prise en charge de tuple est ajoutée au sérialiseur, la fonction to_tuple permettra la sérialisation des structures de données utilisateur. Et il devient possible de les trahir en tant que paramètres et de renvoyer des résultats dans votre RPC.
En plus des structures de données définies par l'utilisateur, C ++ a d'autres types intégrés pour lesquels la sortie vers le flux standard n'est pas implémentée. Le désir de réduire le nombre d'opérateurs de sortie surchargés dans le flux conduit à un code généralisé qui permet à une méthode de traiter la plupart des conteneurs C ++, tels que std :: list, std :: vector, std :: map. Sans oublier SFINAE et std :: enable_if_t, vous pouvez continuer à étendre le sérialiseur. Dans ce cas, il sera nécessaire de déterminer indirectement les propriétés de type d'une manière ou d'une autre, similaire à ce qui est fait dans l'implémentation de is_braces_constructible_v.
Conclusion
Hors de la portée de la publication, l'exception de marshaling, le transport, la sérialisation des conteneurs stl et bien plus encore.
Afin de ne pas compliquer considérablement la publication, seuls des principes généraux ont été donnés sur lesquels j'ai pu créer ma bibliothèque RPC et résoudre moi-même l'ensemble de tâches d'origine - pour essayer de nouvelles fonctionnalités C ++ 14/17. contient des exemples d'utilisation assez détaillés. Code debibliothèque NanoRPC sur GitHub .Merci de votre attention!