
Beberapa tahun yang lalu, pengembang C ++ menerima standar C ++ 11 yang telah lama ditunggu-tunggu, yang membawa banyak hal baru. Dan saya memiliki minat untuk dengan cepat beralih ke penggunaannya dalam tugas sehari-hari. Pergi ke C ++ 14 dan 17 ini bukan. Tampaknya tidak ada set fitur yang akan menarik. Pada musim semi, saya memutuskan untuk melihat inovasi bahasa dan mencoba sesuatu. Untuk bereksperimen dengan inovasi, Anda harus membuat sendiri tugas. Saya tidak perlu berpikir panjang. Diputuskan untuk menulis RPC Anda dengan struktur data khusus sebagai parameter dan tanpa menggunakan makro dan pembuatan kode - semuanya dalam C ++. Ini dimungkinkan berkat fitur baru bahasa tersebut.
Ide, implementasi, umpan balik dengan Reddit, perbaikan - semuanya muncul di musim semi, awal musim panas. Pada akhirnya, mereka berhasil menyelesaikan pos di Habr.
Sudahkah Anda memikirkan RPC Anda sendiri? Mungkin materi posting akan membantu Anda menentukan tujuan, metode, sarana dan memutuskan mendukung yang selesai atau mengimplementasikan sesuatu sendiri ...
Pendahuluan
RPC (panggilan prosedur jarak jauh) bukan topik baru. Ada banyak implementasi dalam berbagai bahasa pemrograman. Implementasi menggunakan berbagai format data dan mode transportasi. Semua ini dapat tercermin dalam beberapa poin:
- Serialisasi / Deserialisasi
- Transportasi
- Eksekusi metode jarak jauh
- Hasil kembali
Implementasi ditentukan oleh tujuan yang diinginkan. Misalnya, Anda dapat menetapkan sendiri tujuan untuk memastikan metode panggilan jarak jauh yang cepat dan mengorbankan kegunaan, atau sebaliknya, untuk memberikan kenyamanan maksimal untuk menulis kode, mungkin kehilangan sedikit performa. Tujuan dan alatnya berbeda ... Saya menginginkan kenyamanan dan kinerja yang dapat diterima.
Implementasi
Berikut adalah beberapa langkah untuk mengimplementasikan RPC di C ++ 14/17, dan penekanan diberikan pada beberapa inovasi bahasa yang menyebabkan materi ini muncul.
Materi ini ditujukan bagi mereka yang karena alasan tertentu tertarik pada RPC mereka, dan, mungkin, sejauh ini, membutuhkan informasi tambahan. Dalam komentar, akan menarik untuk melihat deskripsi pengalaman pengembang lain yang menghadapi tugas serupa.
Serialisasi
Sebelum Anda mulai menulis kode, saya akan membentuk tugas:
- Semua parameter metode dan hasil yang dikembalikan dilewatkan melalui tuple.
- Metode yang disebut sendiri tidak berkewajiban untuk menerima dan mengembalikan tupel.
- Hasil pengemasan tuple harus berupa buffer yang formatnya tidak diperbaiki
Berikut ini adalah kode serializer string yang disederhanakan.
string_serializernamespace rpc::type { using buffer = std::vector<char>; }
Dan kode fungsi utama menunjukkan operasi serializer.
Fungsi utama 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);
Aksen terakreditasiPertama-tama, Anda perlu menentukan buffer yang dengannya seluruh pertukaran data akan dilakukan:
namespace rpc::type { using buffer = std::vector<char>; }
Serializer memiliki metode untuk menyimpan tuple ke buffer (simpan) dan memuatnya dari buffer (memuat)
Metode save mengambil tuple dan mengembalikan 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)}; }
Sebuah tuple adalah templat dengan sejumlah parameter variabel. Pola seperti itu muncul di C ++ 11 dan bekerja dengan baik. Di sini Anda harus melalui semua elemen dari template semacam itu. Mungkin ada beberapa opsi. Saya akan menggunakan salah satu fitur C ++ 14 - urutan bilangan bulat (indeks). Tipe make_index_afterence telah muncul di pustaka standar, yang memungkinkan untuk mendapatkan urutan berikut:
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>;
Yang serupa dapat diimplementasikan dalam C ++ 11, dan kemudian membawanya dari proyek ke proyek.
Urutan indeks seperti itu memungkinkan untuk "melewati" 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()); }
Metode to_string menggunakan beberapa fitur dari standar C ++ terbaru.
Aksen terakreditasiDalam C ++ 14, menjadi mungkin untuk menggunakan otomatis sebagai parameter untuk fungsi lambda. Ini sering kali tidak cukup, misalnya, ketika bekerja dengan algoritma perpustakaan standar.
Konvolusi muncul di C ++ 17, yang memungkinkan Anda menulis kode seperti:
(put_item(std::get<I>(tuple)), ... );
Dalam fragmen yang diberikan, fungsi put_item lambda dipanggil untuk setiap elemen dari tuple yang ditransfer. Ini menjamin urutan independen dari platform dan kompiler. Hal serupa dapat ditulis dalam C ++ 11.
template <typename … T> void unused(T && … ) {}
Tetapi dalam urutan apa elemen-elemen yang akan disimpan akan tergantung pada kompiler.
Banyak alias muncul di pustaka standar C ++ 17, misalnya, decay_t, yang mengurangi catatan formulir:
typename decay<T>::type
Keinginan untuk menulis konstruksi yang lebih pendek memiliki tempatnya. Desain template, di mana beberapa jenis nama dan template ditemukan dalam satu baris, dipisahkan oleh titik dua dan kurung sudut, terlihat menyeramkan. Bagaimana Anda bisa menakuti beberapa kolega Anda. Di masa depan, mereka berjanji untuk mengurangi jumlah tempat di mana Anda perlu menulis templat, ketik nama.
Keinginan untuk keringkasan memberi konstruksi menarik lain dari bahasa "jika constexpr", menghindari menulis banyak spesialisasi pribadi template.
Ada satu hal yang menarik. Banyak yang telah diajarkan bahwa saklar dan konstruksi serupa tidak terlalu bagus dalam hal skalabilitas kode. Lebih disukai menggunakan polimorfisme run-time / compile-time dan overload dengan argumen yang mendukung “pilihan yang tepat”. Dan kemudian "jika constexpr" ... Kemungkinan kekompakan tidak membuat semua orang acuh terhadapnya. Kemungkinan bahasa tidak berarti kebutuhan untuk menggunakannya.
Itu perlu untuk menulis serialisasi terpisah untuk tipe string. Untuk pekerjaan yang mudah dengan string, misalnya, ketika menyimpan ke aliran dan membaca darinya, fungsi std :: dikutip muncul. Ini memungkinkan Anda untuk menyaring string dan memungkinkan untuk menyimpan ke aliran dan memuat tanggal darinya tanpa memikirkan pembatas.
Anda dapat berhenti dengan deskripsi serialisasi untuk saat ini. Deserialization (beban) diimplementasikan dengan cara yang sama.
Transportasi
Transportasi sederhana. Ini adalah fungsi yang menerima dan mengembalikan buffer.
namespace rpc::type {
Membentuk objek seperti "eksekutor" menggunakan std :: bind, fungsi lambda, dll., Anda dapat menggunakan implementasi transportasi apa pun. Detail pelaksanaan transportasi dalam pos ini tidak akan dipertimbangkan. Anda dapat melihat implementasi RPC yang telah selesai, tautan yang akan diberikan di bagian akhir.
Pelanggan
Di bawah ini adalah kode klien uji. Klien membuat permintaan dan mengirimkannya ke server, dengan mempertimbangkan transportasi yang dipilih. Dalam kode pengujian di bawah ini, semua permintaan klien ditampilkan di konsol. Dan pada langkah implementasi selanjutnya, klien sudah akan berkomunikasi langsung dengan server.
Pelanggan 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_; }; }; }
Klien diimplementasikan sebagai kelas templat. Parameter template adalah serializer. Jika perlu, kelas dapat diulang tidak dalam satu template dan diteruskan ke konstruktor objek yang mengimplementasikan serializer.
Dalam implementasi saat ini, konstruktor kelas menerima objek yang mengeksekusi. Kontraktor menyembunyikan implementasi transportasi dengan sendirinya, dan memungkinkan pada titik ini dalam kode untuk tidak memikirkan metode pertukaran data antar proses. Dalam kasus uji, implementasi transportasi menampilkan permintaan ke konsol.
auto executor = [] (rpc::type::buffer buffer) {
Kode khusus belum mencoba mengambil keuntungan dari hasil pekerjaan klien, karena tidak ada tempat untuk mendapatkannya.
Metode panggilan klien:- menggunakan serializer mengemas nama metode yang dipanggil dan parameternya
- menggunakan objek yang mengeksekusi mengirim permintaan ke server dan menerima respons
- meneruskan respons yang diterima ke kelas yang mengambil hasil yang diterima
Implementasi klien dasar sudah siap. Sesuatu yang tersisa. Lebih lanjut tentang ini nanti.
Server
Sebelum mulai mempertimbangkan detail implementasi dari sisi server, saya sarankan untuk melihat dengan cepat dan diagonal pada contoh interaksi klien-server yang sudah selesai.
Untuk kesederhanaan, demonstrasi semuanya dalam satu proses. Implementasi transportasi adalah fungsi lambda yang melewati buffer antara klien dan server.
Interaksi klien-server. Uji kasus #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; }
Dalam implementasi kelas server di atas, hal yang paling menarik adalah konstruktor dan metode eksekusi.
Konstruktor kelas server template <typename ... THandler> server(std::pair<char const *, THandler> const & ... handlers) { auto make_executor = [&packer = packer_] (auto const &handler) { auto executor = [&packer, function = std::function{handler}] (type::buffer buffer) { using meta = detail::function_meta<std::decay_t<decltype(function)>>; typename meta::request_type request; packer.load(buffer, request); auto response = std::apply([&function] (std::string const &, auto && ... args) { return function(std::forward<decltype(args)>(args) ... ); }, std::move(request) ); return packer.save(std::make_tuple(std::move(response))); }; return executor; }; (handlers_.emplace(handlers.first, make_executor(handlers.second)), ... ); }
Konstruktor kelas adalah boilerplate. Pada input dibutuhkan daftar pasangan. Setiap pasangan adalah nama metode dan penangan. Dan karena konstruktor adalah templat dengan sejumlah variabel parameter, saat membuat objek server, semua penangan yang tersedia di server terdaftar segera. Itu akan memungkinkan untuk tidak membuat metode pendaftaran tambahan yang disebut pada penangan server. Dan, pada gilirannya, membebaskan seseorang dari memikirkan apakah objek kelas server akan digunakan dalam lingkungan multi-threaded dan apakah sinkronisasi diperlukan.
Sebuah fragmen dari konstruktor dari kelas server template <typename ... THandler> server(std::pair<char const *, THandler> const & ... handlers) {
Menempatkan banyak penangan heterogen yang lulus ke dalam peta fungsi dari jenis yang sama. Untuk ini, konvolusi juga digunakan, yang membuatnya mudah untuk dimasukkan ke dalam std :: map seluruh set handler yang lulus dalam satu baris tanpa loop dan algoritma
(handlers_.emplace(handlers.first, make_executor(handlers.second)), ... );
Fungsi Lambda yang memungkinkan penggunaan otomatis sebagai parameter membuatnya mudah untuk menerapkan jenis pembungkus yang sama pada penangan. Wraps dari jenis yang sama terdaftar di peta metode yang tersedia di server (std :: map). Saat memproses permintaan, pencarian dilakukan pada kartu tersebut, dan penangan yang sama memanggil penangan yang ditemukan, terlepas dari parameter yang diterima dan hasil yang dikembalikan. Fungsi std :: apply yang muncul di pustaka standar memanggil fungsi yang diteruskan ke sana dengan parameter yang dikirimkan sebagai tuple. Fungsi std :: apply juga dapat diimplementasikan dalam C ++ 11. Sekarang sudah tersedia "out of the box" dan tidak perlu mentransfernya dari proyek ke proyek.
Jalankan metode 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)); }
Mengambil nama fungsi yang dipanggil, mencari metode di peta penangan terdaftar, memanggil penangan dan mengembalikan hasilnya. Semua yang menarik dalam pembungkus disiapkan di konstruktor kelas server. Seseorang mungkin telah memperhatikan pengecualian, dan mungkin muncul pertanyaan: "Apakah pengecualiannya ditangani entah bagaimana?" Ya, dalam implementasi penuh, yang akan diberikan dengan referensi di akhir, pengecualian marshaling disediakan. Di sana untuk menyederhanakan materi, pengecualian tidak diberikan antara klien dan server.
Lihatlah fungsinya
utama 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; }
Ini mengimplementasikan interaksi klien-server penuh. Agar tidak menyulitkan materi, klien dan server bekerja dalam satu proses. Mengganti implementasi pelaksana, Anda dapat menggunakan transportasi yang diperlukan.
Dalam standar C ++ 17, kadang-kadang mungkin untuk tidak menentukan parameter template di instantiation. Dalam fungsi utama di atas, ini digunakan ketika mendaftarkan penangan server (std :: pair tanpa parameter template) dan membuat kode lebih sederhana.
Implementasi RPC dasar sudah siap. Tetap menambahkan kemampuan yang dijanjikan untuk melewati struktur data khusus sebagai parameter dan mengembalikan hasil.
Struktur data khusus
Untuk mentransfer data melintasi batas proses, mereka perlu diserialisasi menjadi sesuatu. Misalnya, Anda dapat menampilkan semuanya ke aliran standar. Banyak yang akan didukung di luar kotak. Untuk struktur data khusus, Anda harus mengimplementasikan sendiri operator keluaran. Setiap struktur membutuhkan operator keluarannya sendiri. Terkadang Anda ingin tidak melakukan ini. Untuk memilah-milah semua bidang struktur dan output masing-masing bidang ke aliran, Anda memerlukan beberapa metode umum. Refleksi dapat membantu dengan baik dalam hal ini. Itu belum di C ++. Anda dapat menggunakan pembuatan kode dan penggunaan campuran makro dan templat. Tapi idenya adalah membuat antarmuka pustaka di C ++ murni.
Belum ada refleksi penuh dalam C ++. Oleh karena itu, solusi di bawah ini dapat digunakan dengan beberapa batasan.
Solusi ini didasarkan pada penggunaan fitur C ++ 17 "binding terstruktur" baru. Seringkali dalam dialog Anda dapat menemukan banyak jargon, jadi saya menolak opsi apa pun untuk nama fitur ini dalam bahasa Rusia.
Di bawah ini adalah solusi yang memungkinkan Anda untuk mentransfer bidang struktur data yang ditransfer ke 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(); } }
Di Internet Anda dapat menemukan banyak solusi serupa.
Banyak dari apa yang digunakan di sini dikatakan di atas, kecuali untuk binding terstruktur. Fungsi to_tuple menerima jenis kustom, menentukan jumlah bidang, dan dengan bantuan binding terstruktur "mentransfer" bidang struktur ke tupel. Dan "jika constexpr" memungkinkan Anda untuk memilih cabang implementasi yang diinginkan. Karena tidak ada refleksi dalam C ++, solusi lengkap yang memperhitungkan semua aspek dari tipe tidak dapat dibangun. Ada batasan pada jenis yang digunakan. Salah satunya - jenisnya harus tanpa konstruktor khusus.
To_tuple menggunakan is_braces_constructible_v. Jenis ini memungkinkan Anda untuk menentukan kemampuan menginisialisasi struktur yang ditransfer menggunakan kurung kurawal dan menentukan jumlah bidang.
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;
Fungsi to_tuple di atas dapat mengubah struktur data pengguna yang berisi tidak lebih dari tiga bidang menjadi tupel. Untuk menambah jumlah kemungkinan "bergeser" bidang struktur, Anda dapat menyalin cabang "jika constexpr" dengan inklusi kecil dari pikiran, atau menggunakan tidak hanya meningkatkan boost.prosesor perpustakaan. Jika Anda memilih opsi kedua, kode akan menjadi sulit dibaca dan akan memungkinkan untuk menggunakan struktur dengan sejumlah besar bidang.
Menerapkan to_tuple dengan boost.preprosesor template <typename T> auto to_tuple(T &&value) { using type = std::decay_t<T>; #define NANORPC_TO_TUPLE_LIMIT_FIELDS 64
Jika Anda pernah mencoba melakukan sesuatu seperti boost.bind untuk C ++ 03, di mana Anda harus membuat banyak implementasi dengan jumlah parameter yang berbeda, maka implementasi to_tuple menggunakan boost.preprosesor tampaknya tidak aneh atau rumit.
Dan jika dukungan tuple ditambahkan ke serializer, fungsi to_tuple akan mengaktifkan serialisasi struktur data pengguna. Dan menjadi mungkin untuk mengkhianati mereka sebagai parameter dan mengembalikan hasil di RPC Anda.
Selain struktur data yang ditentukan pengguna, C ++ memiliki tipe bawaan lain yang output ke aliran standar tidak diimplementasikan. Keinginan untuk mengurangi jumlah operator output yang kelebihan beban dalam aliran mengarah ke kode umum yang memungkinkan satu metode untuk memproses sebagian besar wadah C ++, seperti std :: list, std :: vector, std :: map. Tanpa lupa tentang SFINAE dan std :: enable_if_t, Anda dapat terus memperpanjang serializer. Dalam hal ini, akan diperlukan untuk entah bagaimana menentukan secara tidak langsung properti tipe, mirip dengan apa yang dilakukan dalam implementasi is_braces_constructible_v.
Kesimpulan
Di luar ruang lingkup pos, pengecualian marshaling, transportasi, serialisasi wadah stl dan banyak lagi yang tersisa. Agar tidak menyulitkan posting, hanya prinsip-prinsip umum yang diberikan di mana saya dapat membangun perpustakaan RPC saya dan menyelesaikan tugas asli yang ditetapkan untuk diri saya sendiri - untuk mencoba fitur C ++ 14/17 yang baru. Dan implementasi yang dihasilkan memungkinkan Anda untuk memanggil metode jarak jauh menggunakan HTTP / HTTPS yang tersebar luas dan berisi contoh penggunaan yang cukup rinci. Kodeperpustakaan NanoRPC di GitHub .Terima kasih atas perhatian anda!