أسلوبي في تطبيق المفوضين في C ++: استدعاء دالة ذات معلمات غير معروفة في وقت التشغيل

قبل التاريخ


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

بطبيعة الحال ، فإن تقليد المندوبين بشكل كامل أمر معقد للغاية ، لذلك ستوضح هذه المقالة فقط الهيكل العام للمكتبة والحل لبعض المشاكل الهامة التي تنشأ عند التعامل مع ما لا تدعمه اللغة بشكل مباشر.

استدعاء وظائف مع عدد غير محدد من المعلمات وأنواع غير معروفة أثناء الترجمة


بالطبع ، هذه هي المشكلة الرئيسية في C ++ ، والتي ليس من السهل حلها. بالطبع ، في C ++ هناك أداة موروثة من C - varargs ، وعلى الأرجح هذا هو أول ما يتبادر إلى الذهن ... ومع ذلك ، فهي لا تناسب ، أولاً ، بسبب طبيعتها غير الآمنة من النوع (مثل أشياء كثيرة من C) ، ثانياً ، عند استخدام مثل هذه الوسيطات ، عليك أن تعرف مقدمًا أنواع الوسائط. ومع ذلك ، من شبه المؤكد ، هذه ليست كل المشاكل مع varargs . بشكل عام ، هذه الأداة ليست مساعدًا هنا.

والآن سأدرج الأدوات التي ساعدتني في حل هذه المشكلة.

الأمراض المنقولة جنسيا :: أي


بدءًا من C ++ 17 ، تحتوي اللغة على حاوية حاوية رائعة لأي شيء - بعض التشابه البعيد مع System.Object في CLI هو std :: any . هذه الحاوية يمكنها حقًا تخزين أي شيء ، وحتى كيف: بكفاءة! - يوصى المعيار بتخزين الكائنات الصغيرة فيه مباشرة ، ويمكن بالفعل تخزين الكائنات الكبيرة في الذاكرة الديناميكية (على الرغم من أن هذا السلوك ليس إلزاميًا ، إلا أن Microsoft فعلت ذلك في تطبيق C ++ الخاص به ، وهو خبر سار). وفقط يمكن أن يسمى التشابه لأن System.Object متورط في علاقة الوراثة ("is a") ، و std :: any متورط في علاقة العضوية ("has"). بالإضافة إلى البيانات ، تحتوي الحاوية على مؤشر للكائن std :: type_info - RTTI حول النوع الذي "يكمن" كائنه في الحاوية.

يتم تخصيص ملف رأس كامل <any> للحاوية.

"لسحب" كائن من الحاوية ، تحتاج إلى استخدام دالة القالب std :: any_cast () ، والتي تُرجع مرجعًا إلى الكائن.
مثال للاستخدام:

#include <any> void any_test() { std::any obj = 5; int from_any = std::any_cast<int>(obj); } 

إذا كان النوع المطلوب لا يتطابق مع ما يحتويه الكائن داخل الحاوية ، فسيتم إلقاء استثناء std :: bad_any_cast .

بالإضافة إلى فئات std :: any و std :: bad_any_cast ووظائف std :: any_cast ، يوجد في ملف الرأس وظيفة قالب std :: make_any مشابهة لل std :: make_shared و std :: make_pair وغيرها من الوظائف من هذا النوع.

RTTI


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

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

عادة ، يتم استخدام المصطلح "RTTI" في اتصال مع أنواع متعددة الأشكال. ومع ذلك ، هنا سوف نستخدم هذا المصطلح بمعنى أوسع. على سبيل المثال ، سنأخذ في الاعتبار حقيقة أن كل نوع يحتوي على معلومات حول النوع في وقت التشغيل (على الرغم من أنه لا يمكنك الحصول عليه إلا بشكل ثابت في وقت التحويل البرمجي ، على عكس الأنواع متعددة الأشكال). لذلك ، من الممكن (والضروري) مقارنة أنواع حتى أنواع غير متعددة الأشكال (آسف للتعلم) في وقت التشغيل.
يمكن الوصول إلى RTTI باستخدام فئة std :: type_info . تقع هذه الفئة في ملف الرأس <typeinfo> . يمكن الحصول على مرجع إلى كائن من هذه الفئة (على الأقل في الوقت الحالي) فقط باستخدام عامل التشغيل typeid () .

قوالب


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

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

التفاف الوسيطة متبوعة بمكالمة


لذلك ، لدينا وظيفة معينة تأخذ عدة معلمات كمدخلات.

سأريكم رسمًا للرمز يشرح نواياي.

 #include <Variadic_args_binder.hpp> #include <string> #include <iostream> #include <vector> #include <any> int f(int a, std::string s) { std::cout << "int: " << a << "\nstring: " << s << std::endl; return 1; } void demo() { std::vector<std::any> params; params.push_back(5); params.push_back(std::string{ "Hello, Delegates!" }); delegates::Variadic_args_binder<int(*)(int, std::string), int, std::string> binder{ f, params }; binder(); } 

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

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

 fun_ptr(param1, param2, …, paramN); 

هذه هي الطريقة التي يعمل بها C ++. وكل هذا يعقد إلى حد كبير.

فقط قالب السحر يمكن التعامل معها!

الفكرة الرئيسية هي إنشاء أنواع متكررة تخزن في كل مستوى تداخل إحدى الوسائط أو إحدى الدالات.

لذلك ، قم بتعريف فئة _Tagged_args_binder :

 namespace delegates::impl { template <typename Func_type, typename... T> class _Tagged_args_binder; } 

من أجل "نقل" حزم الأنواع بشكل مريح ، سننشئ نوعًا إضافيًا ، Type_pack_tag (لماذا كانت هناك حاجة لذلك ، سوف يصبح واضحًا قريبًا):

 template <typename... T> struct Type_pack_tag { }; 

الآن نقوم بإنشاء تخصصات للفئة _Tagged_args_binder .

التخصصات الأولية


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

 template <typename Func_type, typename T1, typename... Types_to_construct> class _Tagged_args_binder<Func_type, Type_pack_tag<T1, Types_to_construct...>, Type_pack_tag<>> { public: static_assert(!std::is_same_v<void, T1>, "Void argument is not allowed"); using Ret_type = std::invoke_result_t<Func_type, T1, Types_to_construct...>; _Tagged_args_binder(Func_type func, std::vector<std::any>& args) : ap_arg{ std::move(unihold::reference_any_cast<T1>(args.at(0))) }, ap_caller_part{ func, args } { } auto operator()() { if constexpr(std::is_same_v<void, Ret_type>) { ap_caller_part(std::move(ap_arg)); return; } else { return std::forward<Ret_type>(ap_caller_part(std::move(ap_arg))); } } auto operator()() const { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::move(ap_arg)); return; } else { return std::forward<Ret_type>(ap_caller_part(std::move(ap_arg))); } } private: _Tagged_args_binder<Func_type, Type_pack_tag<Types_to_construct...>, Type_pack_tag<T1>> ap_caller_part; T1 ap_arg; }; 

يتم تخزين الوسيطة الأولى لاستدعاء ap_arg وبقية كائن ap_caller_part العودي هنا . لاحظ أن نوع T1 "نقل" من الحزمة الأولى من الأنواع في هذا الكائن إلى الثاني في "الذيل" للكائن العودية.

التخصص في روابط rvalue:

 template <typename Func_type, typename T1, typename... Types_to_construct> class _Tagged_args_binder<Func_type, Type_pack_tag<T1&&, Types_to_construct...>, Type_pack_tag<>> { using move_ref_T1 = std::add_rvalue_reference_t<std::remove_reference_t<T1>>; public: using Ret_type = std::invoke_result_t<Func_type, move_ref_T1, Types_to_construct>; _Tagged_args_binder(Func_type func, std::vector<std::any>& args) : ap_arg{ std::move(unihold::reference_any_cast<T1>(args.at(0))) }, ap_caller_part{ func, args } { } auto operator()() { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::move(unihold::reference_any_cast<T1>(ap_arg))); } else { return std::forward<Ret_type>(ap_caller_part(std::move(unihold::reference_any_cast<T1>(ap_arg)))); } } auto operator()() const { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::move(unihold::reference_any_cast<T1>(ap_arg))); } else { return std::forward<Ret_type>(ap_caller_part(std::move(unihold::reference_any_cast<T1>(ap_arg)))); } } private: _Tagged_args_binder<Func_type, Type_pack_tag<Types_to_construct...>, Type_pack_tag<move_ref_T1>> ap_caller_part; std::any ap_arg; }; 


ارتباطات القوالب "اليمنى" ليست معاني يمينية حقيقية. هذه هي ما يسمى بـ "الروابط العالمية" ، والتي ، حسب نوع T1 ، تصبح إما T1 و ، أو T1 && . لذلك ، يجب عليك استخدام الحلول البديلة: أولاً ، نظرًا لأن التخصيصات محددة لكلا النوعين من الارتباطات (لم يتم ذكرها بشكل صحيح ، وللأسباب التي سبق ذكرها) وللمعلمات غير المرجعية ، عند إنشاء قالب ، سيتم اختيار التخصص المطلوب ، حتى لو كان ارتباطًا يمينًا ؛ ثانيًا ، لنقل نوع T1 من الحزمة إلى الحزمة ، يتم استخدام الإصدار المصحح من move_ref_T1 ، والذي يتم تحويله إلى رابط rvalue حقيقي.

يتم التخصص مع وصلة طبيعية بنفس الطريقة ، مع التصحيحات اللازمة.

التخصص النهائي


 template <typename Func_type, typename... Param_type> class _Tagged_args_binder<Func_type, Type_pack_tag<>, Type_pack_tag<Param_type...>> { public: using Ret_type = std::invoke_result_t<Func_type, Param_type...>; inline _Tagged_args_binder(Func_type func, std::vector<std::any>& args) : ap_func{ func } { } inline auto operator()(Param_type... param) { if constexpr(std::is_same_v<void, decltype(ap_func(std::forward<Param_type>(param)...))>) { ap_func(std::forward<Param_type>(param)...); return; } else { return std::forward<Ret_type>(ap_func(std::forward<Param_type>(param)...)); } } inline auto operator()(Param_type... param) const { if constexpr(std::is_same_v<void, Ret_type>) { ap_func(param...); return; } else { return std::forward<Ret_type>(ap_func(param...)); } } private: Func_type ap_func; }; 

هذا التخصص مسؤول عن تخزين كائن وظيفي ، وهو في الواقع ملفوف عليه. هذا هو نوع العودية النهائي.

لاحظ كيف يتم استخدام Type_pack_tag هنا. يتم الآن تجميع جميع أنواع المعلمات في الحزمة اليسرى. هذا يعني أنهم جميعا تتم معالجتها وتعبئتها.

الآن ، أعتقد ، يصبح من الواضح لماذا كان من الضروري استخدام Type_pack_tag . الحقيقة هي أن اللغة لن تسمح باستخدام نوعين من الحزم جنبًا إلى جنب ، على سبيل المثال ، مثل هذا:

 template <typename Func_type, typename T1, typename... Types_to_construct, typename... Param_type> class _Tagged_args_binder<Func_type, T1, Types_to_construct..., Param_type...> { }; 

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

التخصصات المتوسطة


من التخصصات المتوسطة ، سأقدم أخيرًا تخصصًا ، مرة أخرى ، لأنواع القيمة ، والباقي هو عن طريق القياس:

 template <typename Func_type, typename T1, typename... Types_to_construct, typename... Param_type> class _Tagged_args_binder<Func_type, Type_pack_tag<T1, Types_to_construct...>, Type_pack_tag<Param_type...>> { public: using Ret_type = std::invoke_result_t<Func_type, Param_type..., T1, Types_to_construct...>; static_assert(!std::is_same_v<void, T1>, "Void argument is not allowed"); inline _Tagged_args_binder(Func_type func, std::vector<std::any>& args) : ap_arg{ std::move(unihold::reference_any_cast<T1>(args.at(sizeof...(Param_type)))) }, ap_caller_part{ func, args } { } inline auto operator()(Param_type... param) { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::forward<Param_type>(param)..., std::move(ap_arg)); return; } else { return std::forward<Ret_type>(ap_caller_part(std::forward<Param_type>(param)..., std::move(ap_arg))); } } inline auto operator()(Param_type... param) const { if constexpr (std::is_same_v<void, Ret_type>) { ap_caller_part(std::forward<Param_type>(param)..., std::move(ap_arg)); } else { return std::forward<Ret_type>(ap_caller_part(std::forward<Param_type>(param)..., std::move(ap_arg))); } } private: _Tagged_args_binder<Func_type, Type_pack_tag<Types_to_construct...>, Type_pack_tag<Param_type..., T1>> ap_caller_part; T1 ap_arg; }; 

هذا التخصص مخصص لحزم أي وسيطة باستثناء الأولى.

الطبقة الموثق


فئة _Tagged_args_binder غير مخصصة للاستخدام المباشر ، والتي أردت التأكيد عليها بتسطير سفلي مفرد في بداية اسمها. لذلك ، سأقدم رمزًا لفئة صغيرة ، وهو نوع من "الواجهة" لهذا النوع القبيح وغير المريح لاستخدام الكتابة (التي ، مع ذلك ، تستخدم حيل C ++ غير المعتادة ، والتي تمنحها بعض السحر ، في رأيي):

 namespace cutecpplib::delegates { template <typename Functor_type, typename... Param_type> class Variadic_args_binder { using binder_type = impl::_Tagged_args_binder<Functor_type, Type_pack_tag<Param_type...>, Type_pack_tag<>>; public: using Ret_type = std::invoke_result_t<binder_type>; inline Variadic_args_binder(Functor_type function, Param_type... param) : ap_tagged_binder{ function, param... } { } inline Variadic_args_binder(Functor_type function, std::vector<std::any>& args) : ap_tagged_binder{ function, args } { } inline auto operator()() { return ap_tagged_binder(); } inline auto operator()() const { return ap_tagged_binder(); } private: binder_type ap_tagged_binder; }; } 

اتفاقية Unihold - تمرير الروابط داخل std :: any


يجب أن يكون القارئ المهتم قد لاحظ أن الشفرة تستخدم الدالة unihold :: reference_any_cast () . تم تصميم هذه الوظيفة ، وكذلك التناظرية unihold :: pointer_any_cast () ، لتنفيذ اتفاقية المكتبة: يتم تمرير الوسائط التي يجب تمريرها بالرجوع بواسطة المؤشر إلى std :: any .

ترجع الدالة reference_any_cast دائمًا مرجعًا إلى كائن ما ، سواء كان الكائن نفسه مخزّنًا في الحاوية أو مؤشرًا إليه فقط. إذا كان std :: any يحتوي على كائن ، فيتم إرجاع مرجع إلى هذا الكائن داخل الحاوية ؛ إذا كان يحتوي على مؤشر ، فسيتم إرجاع مرجع إلى الكائن المشار إليه بواسطة المؤشر.

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

يجب أن تكون الوظائف متخصصة بشكل صريح في نوع الكائن المخزن ، تمامًا مثل تحويلات نوع C ++ ووظائف القالب المماثلة.

رمز هذه الوظائف:

 template <typename T> std::remove_reference_t<T>& unihold::reference_any_cast(std::any& wrapper) { bool result; return reference_any_cast<T>(wrapper, result); } template <typename T> const std::remove_reference_t<T>& unihold::reference_any_cast(const std::any& wrapper) { bool result; return reference_any_cast<T>(wrapper, result); } template <typename T> std::remove_reference_t<T>& unihold::reference_any_cast(std::any& wrapper, bool& is_owner) { auto ptr = pointer_any_cast<T>(&wrapper, is_owner); if (!ptr) throw std::bad_any_cast{ }; return *ptr; } template <typename T> const std::remove_reference_t<T>& unihold::reference_any_cast(const std::any& wrapper, bool& is_owner) { auto ptr = pointer_any_cast<T>(&wrapper, is_owner); if (!ptr) throw std::bad_any_cast{ }; return *ptr; } template <typename T> std::remove_reference_t<T>* unihold::pointer_any_cast(std::any* wrapper, bool& is_owner) { using namespace std; using NR_T = remove_reference_t<T>; // No_reference_T //     wrapper NR_T** double_ptr_to_original = any_cast<NR_T*>(wrapper); //      wrapper NR_T* ptr_to_copy; if (double_ptr_to_original) { // Wrapper      is_owner = false; return *double_ptr_to_original; } else if (ptr_to_copy = any_cast<NR_T>(wrapper)) { // Wrapper    is_owner = true; return ptr_to_copy; } else { throw bad_any_cast{}; } } template <typename T> const std::remove_reference_t<T>* unihold::pointer_any_cast(const std::any* wrapper, bool& is_owner) { using namespace std; using NR_T = remove_reference_t<T>; // No_reference_T //     wrapper NR_T*const * double_ptr_to_original = any_cast<NR_T*>(wrapper); //      wrapper const NR_T* ptr_to_copy; //remove_reference_t<T>* ptr2 = any_cast<remove_reference_t<T>>(&wrapper); if (double_ptr_to_original) { // Wrapper      is_owner = false; return *double_ptr_to_original; } else if (ptr_to_copy = any_cast<NR_T>(wrapper)) { // Wrapper    is_owner = true; return ptr_to_copy; } else { throw bad_any_cast{}; } } template <typename T> std::remove_reference_t<T>* unihold::pointer_any_cast(std::any* wrapper) { bool result; return pointer_any_cast<T>(wrapper, result); } template <typename T> const std::remove_reference_t<T>* unihold::pointer_any_cast(const std::any* wrapper) { bool result; return pointer_any_cast<T>(wrapper, result); } 

استنتاج


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

PS باستخدام RTTI سيتم عرضه في الجزء التالي.

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


All Articles