RPC - eine Gelegenheit, neu in C ++ 14/17 zu testen

Vor einigen Jahren erhielten C ++ - Entwickler den lang erwarteten C ++ 11-Standard, der viele neue Dinge mit sich brachte. Und ich hatte ein Interesse daran, schnell auf den Einsatz bei alltäglichen Aufgaben umzusteigen. Gehen Sie zu C ++ 14 und 17, das war nicht. Es schien, dass es keine Reihe von Funktionen gab, die von Interesse wären. Im Frühjahr habe ich beschlossen, mir die Innovationen der Sprache anzuschauen und etwas auszuprobieren. Um mit Innovationen zu experimentieren, musste man sich eine Aufgabe ausdenken. Ich musste nicht lange nachdenken. Es wurde beschlossen, Ihren RPC mit benutzerdefinierten Datenstrukturen als Parameter und ohne Verwendung von Makros und Codegenerierung zu schreiben - alles in C ++. Dies war dank der neuen Funktionen der Sprache möglich.

Die Idee, Implementierung, Feedback mit Reddit, Verbesserungen - alles erschien im Frühjahr, Frühsommer. Am Ende gelang es ihnen, den Posten auf Habr zu beenden.

Hast du über deinen eigenen RPC nachgedacht? Vielleicht hilft Ihnen das Material des Beitrags dabei, das Ziel, die Methoden und Mittel zu bestimmen und sich für das fertige zu entscheiden oder selbst etwas umzusetzen ...

Einführung


RPC (Remote Procedure Call) ist kein neues Thema. Es gibt viele Implementierungen in verschiedenen Programmiersprachen. Implementierungen verwenden verschiedene Datenformate und Transportmittel. All dies kann sich in einigen Punkten widerspiegeln:

  • Serialisierung / Deserialisierung
  • Transport
  • Remote-Methodenausführung
  • Ergebnis zurückgeben

Die Umsetzung wird durch das gewünschte Ziel bestimmt. Sie können sich beispielsweise das Ziel setzen, eine hohe Geschwindigkeit beim Aufrufen einer Remote-Methode zu gewährleisten und die Benutzerfreundlichkeit zu beeinträchtigen, oder umgekehrt, um maximalen Komfort beim Schreiben von Code zu bieten und möglicherweise ein wenig an Leistung zu verlieren. Die Ziele und Werkzeuge sind unterschiedlich ... Ich wollte Komfort und akzeptable Leistung.

Implementierung


Im Folgenden finden Sie einige Schritte zur Implementierung von RPC in C ++ 14/17. Der Schwerpunkt liegt auf einigen Sprachinnovationen, die zum Erscheinen dieses Materials geführt haben.

Das Material ist für diejenigen gedacht, die aus irgendeinem Grund an ihrem RPC interessiert sind und möglicherweise bis jetzt zusätzliche Informationen benötigen. In den Kommentaren wäre es interessant, eine Beschreibung der Erfahrungen anderer Entwickler zu sehen, die mit ähnlichen Aufgaben konfrontiert sind.

Serialisierung


Bevor Sie anfangen, Code zu schreiben, werde ich eine Aufgabe bilden:

  • Alle Methodenparameter und das zurückgegebene Ergebnis werden durch das Tupel übergeben.
  • Aufgerufene Methoden selbst sind nicht erforderlich, um Tupel zu empfangen und zurückzugeben.
  • Das Ergebnis des Packens eines Tupels sollte ein Puffer sein, dessen Format nicht festgelegt ist

Das Folgende ist der Code für einen vereinfachten String-Serializer.

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 

Und der Hauptfunktionscode, der die Funktionsweise des Serializers demonstriert.

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

Akkreditierte Akzente

Zunächst müssen Sie den Puffer bestimmen, mit dem der gesamte Datenaustausch durchgeführt wird:

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

Der Serializer verfügt über Methoden zum Speichern eines Tupels im Puffer (Speichern) und zum Laden aus dem Puffer (Laden).

Die Speichermethode nimmt ein Tupel und gibt einen Puffer zurück.

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

Ein Tupel ist eine Vorlage mit einer variablen Anzahl von Parametern. Solche Muster erschienen in C ++ 11 und funktionierten gut. Hier müssen Sie irgendwie alle Elemente einer solchen Vorlage durchgehen. Es kann mehrere Optionen geben. Ich werde eine der Funktionen von C ++ 14 verwenden - eine Folge von ganzen Zahlen (Indizes). Der Typ make_index_sequence wurde in der Standardbibliothek angezeigt, mit der die folgende Sequenz abgerufen werden kann:

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

Ein ähnliches kann in C ++ 11 implementiert und dann von Projekt zu Projekt übertragen werden.

Eine solche Folge von Indizes ermöglicht es, das Tupel zu "durchlaufen":

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

Die to_string-Methode verwendet mehrere Funktionen der neuesten C ++ - Standards.

Akkreditierte Akzente

In C ++ 14 wurde es möglich, auto als Parameter für Lambda-Funktionen zu verwenden. Dies reichte beispielsweise bei der Arbeit mit den Algorithmen der Standardbibliothek oft nicht aus.

In C ++ 17 wurde eine Faltung angezeigt, mit der Sie Code schreiben können, z.

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

In dem gegebenen Fragment wird die Lambda-Funktion put_item für jedes der Elemente des übertragenen Tupels aufgerufen. Dies garantiert eine von der Plattform und dem Compiler unabhängige Sequenz. Ähnliches könnte in C ++ 11 geschrieben werden.

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

In welcher Reihenfolge die Elemente gespeichert werden, hängt jedoch vom Compiler ab.

In der C ++ 17-Standardbibliothek wurden viele Aliase angezeigt, z. B. zerfall_t, wodurch die Datensätze des Formulars reduziert wurden:

 typename decay<T>::type 

Der Wunsch, kürzere Konstruktionen zu schreiben, hat einen Platz. Das Vorlagendesign, bei dem sich einige Typnamen und Vorlagen in einer Zeile befinden, die durch Doppelpunkte und spitze Klammern getrennt sind, sieht gruselig aus. Wie können Sie einige Ihrer Kollegen erschrecken? In Zukunft versprechen sie, die Anzahl der Stellen zu reduzieren, an denen Sie Vorlage und Typname schreiben müssen.

Der Wunsch nach Prägnanz gab eine weitere interessante Konstruktion der Sprache "if constexpr", die es vermeidet, viele private Spezialisierungen von Vorlagen zu schreiben.

Es gibt einen interessanten Punkt. Vielen wurde beigebracht, dass Switch- und ähnliche Konstrukte hinsichtlich der Skalierbarkeit von Code nicht sehr gut sind. Es ist vorzuziehen, Laufzeit- / Kompilierungszeit-Polymorphismus und Überladung mit Argumenten zugunsten der „richtigen Wahl“ zu verwenden. Und dann "if constexpr" ... Die Möglichkeit der Kompaktheit lässt nicht jeden gleichgültig. Die Möglichkeit der Sprache bedeutet nicht, dass sie verwendet werden muss.

Es war notwendig, eine separate Serialisierung für den Zeichenfolgentyp zu schreiben. Zum bequemen Arbeiten mit Zeichenfolgen, z. B. beim Speichern in einem Stream und Lesen daraus, wurde die Funktion std :: quote angezeigt. Sie können damit Zeichenfolgen überprüfen und in einem Stream speichern und Daten daraus laden, ohne an das Trennzeichen denken zu müssen.

Sie können vorerst mit der Beschreibung der Serialisierung aufhören. Die Deserialisierung (Last) wird ähnlich implementiert.

Transport


Der Transport ist einfach. Dies ist eine Funktion, die einen Puffer empfängt und zurückgibt.

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

Wenn Sie ein solches Objekt "executor" mit std :: bind, Lambda-Funktionen usw. bilden, können Sie jede Ihrer Transportimplementierungen verwenden. Einzelheiten zur Durchführung des Verkehrs innerhalb dieser Stelle werden nicht berücksichtigt. Sie können sich die abgeschlossene RPC-Implementierung ansehen, auf die am Ende verwiesen wird.

Kunde


Unten finden Sie einen Test-Client-Code. Der Client generiert Anforderungen und sendet sie unter Berücksichtigung des ausgewählten Transports an den Server. Im folgenden Testcode werden alle Clientanforderungen auf der Konsole angezeigt. Und im nächsten Schritt der Implementierung kommuniziert der Client bereits direkt mit dem Server.

Kunde
 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 

Der Client ist als Vorlagenklasse implementiert. Der Template-Parameter ist ein Serializer. Bei Bedarf kann die Klasse nicht in der Vorlage 1 wiederholt und ein Objekt an den Konstruktor übergeben werden, das den Serializer implementiert.

In der aktuellen Implementierung akzeptiert der Klassenkonstruktor ein ausführendes Objekt. Der Auftragnehmer verbirgt die Implementierung des Transports unter sich und ermöglicht es an dieser Stelle im Code, nicht über Methoden zum Datenaustausch zwischen Prozessen nachzudenken. Im Testfall zeigt die Transportimplementierung Anforderungen an die Konsole an.

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

Der benutzerdefinierte Code hat noch nicht versucht, das Ergebnis der Arbeit des Kunden zu nutzen, da es keinen Ort gibt, an dem er abgerufen werden kann.

Client-Aufrufmethode:

  • Mit dem Serializer werden der Name der aufgerufenen Methode und ihre Parameter gepackt
  • Die Verwendung des ausführenden Objekts sendet eine Anforderung an den Server und empfängt eine Antwort
  • Übergibt die empfangene Antwort an eine Klasse, die das empfangene Ergebnis abruft

Die grundlegende Client-Implementierung ist fertig. Es bleibt noch etwas übrig. Dazu später mehr.

Server


Bevor ich mich mit den Implementierungsdetails der Serverseite befasse, schlage ich einen kurzen, diagonalen Blick auf das fertige Beispiel der Client-Server-Interaktion vor.

Der Einfachheit halber ist die Demonstration alles in einem Prozess. Die Transportimplementierung ist eine Lambda-Funktion, die einen Puffer zwischen Client und Server übergibt.

Client-Server-Interaktion. Testfall
 #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; } 

In der obigen Implementierung der Serverklasse ist der Konstruktor und die Ausführungsmethode das Interessanteste.

Serverklassenkonstruktor

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

Der Konstruktor der Klasse ist Boilerplate. Es akzeptiert eine Liste von Paaren am Eingang. Jedes Paar ist ein Methodenname und ein Handler. Und da der Konstruktor eine Vorlage mit einer variablen Anzahl von Parametern ist, werden beim Erstellen des Serverobjekts alle auf dem Server verfügbaren Handler sofort registriert. Dadurch können keine zusätzlichen Registrierungsmethoden für die Server-Handler aufgerufen werden. Dies befreit Sie wiederum davon, darüber nachzudenken, ob das Serverklassenobjekt in einer Multithread-Umgebung verwendet wird und ob eine Synchronisierung erforderlich ist.

Ein Fragment des Konstruktors der Serverklasse

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

Fügt viele übergebene heterogene Handler in die Karte der Funktionen desselben Typs ein. Hierzu wird auch die Faltung verwendet, die es einfach macht, den gesamten Satz übergebener Handler in einer Zeile ohne Schleifen und Algorithmen in die std :: map einzufügen

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

Lambda-Funktionen, die die Verwendung von Auto als Parameter ermöglichen, machten es einfach, denselben Wrapper-Over-Handler-Typ zu implementieren. Wraps desselben Typs werden in der Karte der auf dem Server verfügbaren Methoden (std :: map) registriert. Bei der Verarbeitung von Anforderungen wird eine Suche auf einer solchen Karte durchgeführt, und derselbe Handler ruft den gefundenen Handler auf, unabhängig von den empfangenen Parametern und dem zurückgegebenen Ergebnis. Die in der Standardbibliothek angezeigte Funktion std :: apply ruft die an sie übergebene Funktion mit den als Tupel übergebenen Parametern auf. Die Funktion std :: apply kann auch in C ++ 11 implementiert werden. Jetzt ist es sofort einsatzbereit und muss nicht mehr von Projekt zu Projekt übertragen werden.

Methode ausführen

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

Ruft den Namen der aufgerufenen Funktion ab, sucht in der Zuordnung der registrierten Handler nach der Methode, ruft den Handler auf und gibt das Ergebnis zurück. Alles interessant in den Wrappern, die im Konstruktor der Serverklasse vorbereitet wurden. Jemand hat die Ausnahme vielleicht bemerkt, und vielleicht stellte sich die Frage: "Werden die Ausnahmen irgendwie behandelt?" Ja, in der vollständigen Implementierung, auf die am Ende Bezug genommen wird, wird Ausnahme-Marshalling bereitgestellt. Genau dort, um das Material zu vereinfachen, werden keine Ausnahmen zwischen Client und Server übergeben.

Schauen Sie sich die Funktion noch einmal an

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

Es implementiert eine vollwertige Client-Server-Interaktion. Um das Material nicht zu komplizieren, arbeiten Client und Server in einem Prozess. Wenn Sie die Implementierung von Executor ersetzen, können Sie den erforderlichen Transport verwenden.

Im C ++ 17-Standard ist es manchmal möglich, bei der Instanziierung keine Vorlagenparameter anzugeben. In der obigen Hauptfunktion wird dies beim Registrieren von Server-Handlern (std :: pair ohne Vorlagenparameter) verwendet und vereinfacht den Code.

Die grundlegende RPC-Implementierung ist fertig. Es bleibt die versprochene Möglichkeit hinzuzufügen, benutzerdefinierte Datenstrukturen als Parameter zu übergeben und Ergebnisse zurückzugeben.

Benutzerdefinierte Datenstrukturen


Um Daten über die Prozessgrenze hinweg zu übertragen, müssen sie in etwas serialisiert werden. Sie können beispielsweise alles in einen Standard-Stream ausgeben. Viel wird sofort unterstützt. Für benutzerdefinierte Datenstrukturen müssen Sie die Ausgabeoperatoren selbst implementieren. Jede Struktur benötigt einen eigenen Ausgabeoperator. Manchmal möchten Sie dies nicht tun. Um alle Felder der Struktur zu sortieren und jedes Feld an den Stream auszugeben, benötigen Sie eine verallgemeinerte Methode. Reflexion könnte dabei gut helfen. Es ist noch nicht in C ++. Sie können auf die Codegenerierung und die Verwendung einer Mischung aus Makros und Vorlagen zurückgreifen. Die Idee war jedoch, die Bibliotheksschnittstelle in reinem C ++ zu erstellen.

In C ++ gibt es noch keine vollständige Reflexion. Daher kann die folgende Lösung mit einigen Einschränkungen verwendet werden.

Die Lösung basiert auf der Verwendung der neuen C ++ 17-Funktion „Strukturierte Bindungen“. In Dialogen findet man oft viel Jargon, daher habe ich keine Optionen für den Namen dieser Funktion auf Russisch abgelehnt.

Im Folgenden finden Sie eine Lösung, mit der Sie die Felder der übertragenen Datenstruktur in das Tupel übertragen können.

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

Im Internet finden Sie viele ähnliche Lösungen.

Vieles, was hier verwendet wurde, wurde oben gesagt, außer für strukturierte Bindungen. Die Funktion to_tuple akzeptiert einen benutzerdefinierten Typ, bestimmt die Anzahl der Felder und "überträgt" mithilfe strukturierter Bindungen die Strukturfelder an ein Tupel. Mit „if constexpr“ können Sie den gewünschten Implementierungszweig auswählen. Da es in C ++ keine Reflexion gibt, kann keine vollständige Lösung erstellt werden, die alle Aspekte des Typs berücksichtigt. Es gibt Einschränkungen für die verwendeten Typen. Einer davon - der Typ sollte ohne benutzerdefinierte Konstruktoren sein.

To_tuple verwendet is_braces_constructible_v. Mit diesem Typ können Sie die Möglichkeit bestimmen, die übertragene Struktur mit geschweiften Klammern zu initialisieren und die Anzahl der Felder zu bestimmen.

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; 

Die obige Funktion to_tuple kann Benutzerdatenstrukturen, die nicht mehr als drei Felder enthalten, in Tupel umwandeln. Um die mögliche Anzahl von "verschobenen" Strukturfeldern zu erhöhen, können Sie entweder die "if constexpr" -Zweige mit einer kleinen Einbeziehung des Geistes kopieren oder nicht die einfachste boost.preprocessor-Bibliothek verwenden. Wenn Sie die zweite Option auswählen, wird der Code schwer lesbar und ermöglicht die Verwendung von Strukturen mit einer großen Anzahl von Feldern.

Implementierung von to_tuple mit 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 } 

Wenn Sie jemals versucht haben, etwas wie boost.bind für C ++ 03 zu tun, wo Sie viele Implementierungen mit einer anderen Anzahl von Parametern vornehmen mussten, erscheint die Implementierung von to_tuple mit boost.preprocessor nicht seltsam oder kompliziert.

Wenn dem Serializer Tupelunterstützung hinzugefügt wird, ermöglicht die Funktion to_tuple die Serialisierung von Benutzerdatenstrukturen. Und es wird möglich, sie als Parameter zu verraten und Ergebnisse in Ihrem RPC zurückzugeben.

Neben benutzerdefinierten Datenstrukturen verfügt C ++ über weitere integrierte Typen, für die keine Ausgabe an den Standarddatenstrom implementiert ist. Der Wunsch, die Anzahl der überladenen Ausgabeoperatoren im Stream zu reduzieren, führt zu einem verallgemeinerten Code, mit dem eine Methode die meisten C ++ - Container wie std :: list, std :: vector, std :: map verarbeiten kann. Ohne SFINAE und std :: enable_if_t zu vergessen, können Sie den Serializer weiter erweitern. In diesem Fall müssen die Eigenschaften von Typen irgendwie indirekt bestimmt werden, ähnlich wie bei der Implementierung von is_braces_constructible_v.

Fazit


Außerhalb des Geltungsbereichs der Post blieben Ausnahme-Marshalling, Transport, Serialisierung von STL-Containern und vieles mehr.Um den Beitrag nicht wesentlich zu komplizieren, wurden nur allgemeine Prinzipien angegeben, auf denen ich meine RPC-Bibliothek aufbauen und den ursprünglichen Aufgabensatz für mich selbst lösen konnte - um neue C ++ 14/17-Funktionen auszuprobieren. Mit der resultierenden Implementierung können Sie Remote-Methoden mit dem weit verbreiteten HTTP / HTTPS und aufrufen enthält ziemlich detaillierte Verwendungsbeispiele. NanoRPC-

Bibliothekscode auf GitHub .

Vielen Dank für Ihre Aufmerksamkeit!

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


All Articles