RPC - مناسبة لتجربة الجديد في C ++ 14/17

منذ عدة سنوات ، تلقى مطورو C ++ معيار C ++ 11 الذي طال انتظاره ، والذي جلب الكثير من الأشياء الجديدة. وكان لدي اهتمام بالتحول بسرعة إلى استخدامه في المهام اليومية. انتقل إلى C ++ 14 و 17 لم يكن هذا. يبدو أنه لا توجد مجموعة من الميزات التي قد تكون ذات أهمية. في الربيع ، قررت النظر إلى ابتكارات اللغة وتجربة شيء ما. لتجربة الابتكارات ، كان عليك أن تأتي بمهمة لنفسك. لم يكن علي التفكير طويلا. تقرر كتابة RPC الخاص بك بهياكل بيانات مخصصة كمعلمات وبدون استخدام وحدات الماكرو وإنشاء التعليمات البرمجية - كل ذلك في C ++. كان ذلك ممكنا بفضل الميزات الجديدة للغة.

الفكرة والتنفيذ والتغذية الراجعة مع Reddit والتحسينات - ظهر كل شيء في الربيع وأوائل الصيف. في النهاية ، تمكنوا من إنهاء المنشور على هبر.

هل فكرت في RPC الخاص بك؟ ربما تساعدك مادة المنشور في تحديد الهدف والأساليب والوسائل وتقرر لصالح الهدف النهائي أو تنفيذ شيء بنفسك ...

مقدمة


RPC (استدعاء الإجراء البعيد) ليس موضوعًا جديدًا. هناك العديد من التطبيقات بلغات برمجة مختلفة. تستخدم عمليات التنفيذ تنسيقات البيانات المختلفة وأنماط النقل. يمكن أن ينعكس كل هذا في بضع نقاط:

  • التسلسل / إلغاء التسلسل
  • النقل
  • تنفيذ الأسلوب البعيد
  • نتيجة الإرجاع

يتم تحديد التنفيذ من خلال الهدف المطلوب. على سبيل المثال ، يمكنك أن تحدد لنفسك هدف ضمان سرعة عالية لاستدعاء طريقة بعيدة والتضحية بقابلية الاستخدام ، أو العكس ، لتوفير أقصى قدر من الراحة لكتابة التعليمات البرمجية ، وربما فقدان القليل في الأداء. الأهداف والأدوات مختلفة ... أردت الراحة والأداء المقبول.

التنفيذ


فيما يلي بعض الخطوات لتنفيذ RPC في C ++ 14/17 ، ويتم التركيز على بعض ابتكارات اللغة التي تسببت في ظهور هذه المادة.

تم إعداد هذه المادة لأولئك الذين لسبب ما مهتمون بـ RPC الخاص بهم ، وربما يحتاجون حتى الآن إلى معلومات إضافية. في التعليقات ، سيكون من المثير للاهتمام رؤية وصف لتجربة المطورين الآخرين الذين يواجهون مهامًا مماثلة.

تسلسل


قبل البدء في كتابة التعليمات البرمجية ، سأشكل مهمة:

  • يتم تمرير جميع معلمات الطريقة والنتيجة المرتجعة من خلال tuple.
  • الأساليب المطلوبة نفسها ليست ملزمة بقبول وإعادة الصفوف.
  • يجب أن تكون نتيجة تعبئة tuple عازلة لم يتم إصلاح تنسيقها

ما يلي هو رمز مُسلسل سلسلة مبسط.

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 

ورمز الوظيفة الرئيسي يوضح تشغيل التسلسل.

الوظيفة الرئيسية
 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; } 

اللهجات المعتمدة

بادئ ذي بدء ، تحتاج إلى تحديد المخزن المؤقت الذي سيتم من خلاله تبادل البيانات بالكامل:

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

لدى المُسلسِل طرق لحفظ مجموعة tuple في المخزن المؤقت (حفظ) وتحميلها من المخزن المؤقت (تحميل)

يأخذ أسلوب الحفظ مجموعة ويعيد المخزن المؤقت.

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

المجموعة هي قالب يحتوي على عدد متغير من المعلمات. ظهرت هذه الأنماط في C ++ 11 وعملت بشكل جيد. هنا تحتاج إلى الذهاب إلى حد ما من خلال جميع عناصر مثل هذا القالب. قد يكون هناك العديد من الخيارات. سأستخدم إحدى ميزات C ++ 14 - سلسلة من الأعداد الصحيحة (المؤشرات). ظهر نوع make_index_sequence في المكتبة القياسية ، مما يسمح بالحصول على التسلسل التالي:

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

يمكن تنفيذ مماثل في C ++ 11 ، ثم نقله من مشروع إلى آخر.

هذا التسلسل من المؤشرات يجعل من الممكن "المرور" بالصفقة:

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

تستخدم طريقة to_string العديد من الميزات لأحدث معايير C ++.

اللهجات المعتمدة

في C ++ 14 ، أصبح من الممكن استخدام السيارات كمعلمات لوظائف لامدا. غالبًا ما لم يكن هذا كافيًا ، على سبيل المثال ، عند العمل مع خوارزميات المكتبة القياسية.

ظهر التفاف في C ++ 17 ، والذي يسمح لك بكتابة كود مثل:

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

في الجزء المحدد ، يتم استدعاء دالة put_item lambda لكل عنصر من عناصر المجموعة المنقولة. هذا يضمن تسلسل مستقل عن المنصة والمترجم. يمكن كتابة شيء مشابه في C ++ 11.

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

ولكن بأي ترتيب يتم تخزين العناصر يعتمد على المترجم.

ظهرت العديد من الأسماء المستعارة في المكتبة القياسية C ++ 17 ، على سبيل المثال ، decay_t ، مما قلل من سجلات النموذج:

 typename decay<T>::type 

الرغبة في كتابة منشآت أقصر لها مكان يجب أن تكون. يبدو تصميم القالب ، حيث يوجد زوجان من أسماء القوالب والقالب في سطر واحد ، مفصولين بنقطتين وأقواس زاوية ، مخيفة. كيف تخيف بعض زملائك. في المستقبل ، يعدون بتقليل عدد الأماكن التي تحتاج إلى كتابة النموذج فيها ، واسم الكتابة.

أعطت الرغبة في الإيجاز بنية أخرى مثيرة للاهتمام للغة "if constexpr" ، تتجنب كتابة العديد من التخصصات الخاصة للقوالب.

هناك نقطة مثيرة للاهتمام. تم تعليم الكثير أن التبديل والبنى المماثلة ليست جيدة جدًا من حيث قابلية تطوير الكود. من الأفضل استخدام تعدد الأشكال في وقت التشغيل / الترجمة و التحميل الزائد بالحجج لصالح "الاختيار الصحيح". ومن ثم "إذا كونستكسبر" ... فإن إمكانية الدمج لا تترك الجميع غير مبالين به. لا تعني إمكانية اللغة الحاجة إلى استخدامها.

كان من الضروري كتابة تسلسل منفصل لنوع السلسلة. للعمل المريح مع السلاسل ، على سبيل المثال ، عند الحفظ في دفق وقراءة منه ، ظهرت الوظيفة std :: quoted. يسمح لك بفحص السلاسل ويجعل من الممكن الحفظ في دفق وتحميل التواريخ منه دون التفكير في المحدد.

يمكنك التوقف عن وصف التسلسل في الوقت الحالي. يتم تنفيذ إلغاء التسلسل (الحمل) بالمثل.

النقل


النقل بسيط. هذه وظيفة تستقبل وتعيد المخزن المؤقت.

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

تشكيل مثل هذا الكائن "المنفذ" باستخدام std :: bind ، وظائف lambda ، وما إلى ذلك ، يمكنك استخدام أي من تطبيقات النقل الخاصة بك. لن يتم النظر في تفاصيل تنفيذ النقل داخل هذه الوظيفة. يمكنك إلقاء نظرة على تنفيذ RPC المكتمل ، والذي سيتم تقديم رابط إليه في النهاية.

الزبون


يوجد أدناه رمز اختبار عميل. يقوم العميل بإنشاء الطلبات وإرسالها إلى الخادم ، مع مراعاة النقل المحدد. في رمز الاختبار أدناه ، يتم عرض جميع طلبات العملاء على وحدة التحكم. وفي الخطوة التالية من التنفيذ ، سيتواصل العميل بالفعل مع الخادم.

الزبون
 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 

يتم تنفيذ العميل كفئة قالب. معلمة القالب هي مُسلسل. إذا لزم الأمر ، يمكن إعادة تصميم الصف ليس في القالب الأول وتمريره إلى المنشئ كائن يقوم بتنفيذ المتسلسل.

في التنفيذ الحالي ، يقبل مُنشئ الفئة كائن تنفيذي. يخفي المقاول تنفيذ النقل في حد ذاته ، ويجعل من الممكن في هذه المرحلة من الرمز عدم التفكير في طرق لتبادل البيانات بين العمليات. في حالة الاختبار ، يعرض تنفيذ النقل الطلبات إلى وحدة التحكم.

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

لم يحاول الرمز المخصص حتى الآن الاستفادة من نتيجة عمل العميل ، حيث لا يوجد مكان للحصول عليه.

طريقة الاتصال بالعميل:

  • باستخدام أداة التسلسل يحزم اسم الطريقة المطلوبة ومعلماتها
  • باستخدام الكائن التنفيذي يرسل طلب إلى الخادم ويتلقى استجابة
  • تمرير الاستجابة المتلقاة إلى فصل يسترد النتيجة المتلقاة

تنفيذ العميل الأساسي جاهز. بقي شيء آخر. المزيد عن هذا في وقت لاحق.

الخادم


قبل البدء في النظر في تفاصيل التنفيذ من جانب الخادم ، أقترح نظرة سريعة وقطرية على المثال المكتمل للتفاعل بين الخادم والعميل.

من أجل البساطة ، فإن التظاهرة كلها في عملية واحدة. تنفيذ النقل هو وظيفة لامدا التي تمر المخزن المؤقت بين العميل والخادم.

التفاعل بين العميل والخادم. حالة الاختبار
 #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; } 

في التنفيذ أعلاه لفئة الخادم ، الشيء الأكثر إثارة للاهتمام هو المنشئ وطريقة التنفيذ.

مُنشئ فئة الخادم

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

مُنشئ الصنف هو مرجل. عند الإدخال ، يأخذ قائمة أزواج. كل زوج هو اسم أسلوب ومعالج. وبما أن المُنشئ هو قالب يحتوي على عدد متغير من المعلمات ، عند إنشاء كائن الخادم ، يتم تسجيل جميع المعالجات المتوفرة على الخادم على الفور. سيجعل ذلك من الممكن عدم إجراء طرق تسجيل إضافية تسمى معالجات الخادم. وبدوره ، يحرر المرء من التفكير فيما إذا كان سيتم استخدام كائن فئة الخادم في بيئة متعددة الخيوط وما إذا كانت هناك حاجة إلى المزامنة.

جزء من مُنشئ فئة الخادم

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

يضع الكثير من المعالجات غير المتجانسة التي تم تمريرها في خريطة الوظائف من نفس النوع. لهذا ، يتم استخدام الالتفاف أيضًا ، مما يجعل من السهل وضع مجموعة كاملة من المعالجات المارة في سطر واحد بدون حلقات وخوارزميات

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

وظائف Lambda التي تسمح باستخدام السيارات كمعلمات جعلت من السهل تنفيذ نفس نوع الغلاف على معالجات. يتم تسجيل الأغطية من نفس النوع في خريطة الطرق المتاحة على الخادم (std :: map). عند معالجة الطلبات ، يتم إجراء بحث على هذه البطاقة ، ويستدعي المعالج نفسه المعالج الموجود ، بغض النظر عن المعلمات المستلمة والنتيجة المرتجعة. تستدعي الدالة std :: application التي ظهرت في المكتبة القياسية الوظيفة التي تم تمريرها إليها مع المعلمات التي تم تمريرها كمجموعة. يمكن أيضًا تنفيذ دالة std :: application في C ++ 11. الآن هو متاح "خارج الصندوق" وليس هناك حاجة لنقله من مشروع إلى آخر.

طريقة التنفيذ

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

يسترجع اسم الوظيفة المطلوبة ، ويبحث عن الطريقة في خريطة المعالجات المسجلة ، ويدعو المعالج ويعيد النتيجة. كل شيء مثير للاهتمام في الأغلفة المعدة في مُنشئ فئة الخادم. ربما لاحظ شخص ما الاستثناء ، وربما نشأ السؤال: "هل تتم معالجة الاستثناءات بطريقة أو بأخرى؟" نعم ، في التنفيذ الكامل ، والذي سيتم تقديمه بالإشارة في النهاية ، يتم توفير الاستثناء. هناك حقًا لتبسيط المادة ، لا يتم تمرير الاستثناءات بين العميل والخادم.

ألق نظرة أخرى على الوظيفة

الرئيسية
 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; } 

ينفذ التفاعل الكامل بين العميل والخادم. من أجل عدم تعقيد المادة ، يعمل العميل والخادم في عملية واحدة. استبدال تنفيذ المنفذ ، يمكنك استخدام النقل الضروري.

في معيار C ++ 17 ، من الممكن في بعض الأحيان عدم تحديد معلمات القالب عند النسخ. في الوظيفة الرئيسية أعلاه ، يتم استخدام هذا عند تسجيل معالجات الخادم (std :: pair بدون معلمات القالب) ويجعل الشفرة أبسط.

تنفيذ RPC الأساسي جاهز. يبقى لإضافة القدرة الموعودة لتمرير هياكل البيانات المخصصة كمعلمات وإرجاع النتائج.

هياكل البيانات المخصصة


لنقل البيانات عبر حدود العملية ، يجب إجراء تسلسل لها إلى شيء ما. على سبيل المثال ، يمكنك إخراج كل شيء إلى دفق قياسي. سيتم دعم الكثير من خارج منطقة الجزاء. بالنسبة لهياكل البيانات المخصصة ، سيكون عليك تنفيذ عوامل الإخراج بنفسك. يحتاج كل هيكل لمشغل الإخراج الخاص به. في بعض الأحيان لا تريد القيام بذلك. للفرز عبر جميع حقول الهيكل وإخراج كل حقل إلى الدفق ، تحتاج إلى طريقة عامة. يمكن أن يساعد التأمل بشكل جيد في ذلك. ليس بعد في C ++. يمكنك اللجوء إلى إنشاء التعليمات البرمجية واستخدام مزيج من وحدات الماكرو والقوالب. لكن الفكرة كانت جعل واجهة المكتبة في C ++ خالص.

لا يوجد انعكاس كامل في C ++ حتى الآن. لذلك ، يمكن استخدام الحل أدناه مع بعض القيود.

يعتمد الحل على استخدام ميزة "الارتباطات المنظمة" الجديدة C ++ 17. غالبًا ما تجد في الحوارات الكثير من المصطلحات ، لذلك رفضت أي خيارات لاسم هذه الميزة باللغة الروسية.

فيما يلي حل يتيح لك نقل حقول بنية البيانات المنقولة إلى المجموعة.

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

على الإنترنت يمكنك العثور على العديد من الحلول المماثلة.

قيل الكثير من ما تم استخدامه هنا أعلاه ، باستثناء الارتباطات المنظمة. تقبل الدالة to_tuple نوعًا مخصصًا ، وتحدد عدد الحقول ، وبمساعدة عمليات الربط المنظمة "تنقل" حقول البنية إلى مجموعة. و "if constexpr" يسمح لك باختيار فرع التنفيذ المطلوب. نظرًا لعدم وجود انعكاس في C ++ ، لا يمكن بناء حل كامل يأخذ في الاعتبار جميع جوانب النوع. هناك قيود على الأنواع المستخدمة. واحد منهم - يجب أن يكون النوع بدون منشآت مخصصة.

يستخدم To_tuple is_braces_constructible_v. يسمح لك هذا النوع بتحديد القدرة على تهيئة البنية المنقولة باستخدام الأقواس المتعرجة وتحديد عدد الحقول.

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; 

يمكن للدالة to_tuple أعلاه تحويل هياكل بيانات المستخدم التي لا تحتوي على أكثر من ثلاثة حقول إلى صفوف. لزيادة العدد المحتمل للحقول "المنقولة" للهيكل ، يمكنك إما نسخ فروع "if constexpr" مع تضمين بسيط للعقل ، أو اللجوء إلى استخدام ليس مكتبة أبسط معزز. إذا حددت الخيار الثاني ، فستصبح قراءة الرمز صعبة وستجعل من الممكن استخدام الهياكل التي تحتوي على عدد كبير من الحقول.

تنفيذ to_tuple مع 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 } 

إذا كنت قد حاولت فعلًا القيام بشيء مثل boost.bind لـ C ++ 03 ، حيث كان عليك إجراء العديد من عمليات التنفيذ بعدد مختلف من المعلمات ، فإن تنفيذ to_tuple باستخدام boost.preprocessor لا يبدو غريبًا أو معقدًا.

وإذا تمت إضافة دعم tuple إلى المُسلسِل ، فإن وظيفة to_tuple ستمكن تسلسل هياكل بيانات المستخدم. ويصبح من الممكن خيانة هذه المعلمات وإرجاع النتائج في RPC الخاص بك.

بالإضافة إلى تراكيب البيانات المعرفة من قبل المستخدم ، يحتوي C ++ على أنواع مضمنة أخرى لا يتم تنفيذ الإخراج إلى الدفق القياسي. تؤدي الرغبة في تقليل عدد مشغلي المخرجات الزائدة في الدفق إلى رمز معمم يسمح لطريقة واحدة بمعالجة معظم حاويات C ++ ، مثل std :: list و std :: vector و std :: map. دون نسيان SFINAE و std :: enable_if_t ، يمكنك الاستمرار في تمديد المُسلسِل. في هذه الحالة ، سيكون من الضروري تحديد خصائص الأنواع بطريقة غير مباشرة بطريقة أو بأخرى ، على غرار ما يتم في تنفيذ is_braces_constructible_v.

الخلاصة


خارج نطاق المنشور هو استثناء التنظيم والنقل وتسلسل الحاويات stl وأكثر من ذلك بكثير. من أجل عدم تعقيد المنشور بشكل كبير ، تم إعطاء المبادئ العامة فقط التي تمكنت من إنشاء مكتبة RPC الخاصة بي وحل مجموعة المهام الأصلية لنفسي - لتجربة ميزات C ++ 14/17 الجديدة. ويتيح لك التطبيق الناتج استدعاء الطرق البعيدة باستخدام HTTP / HTTPS واسع النطاق و يحتوي على أمثلة استخدام مفصلة إلى حد ما. كود

مكتبة NanoRPC على جيثب .

شكرا لكم على اهتمامكم!

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


All Articles