في الآونة الأخيرة ، أثناء العمل على إصدار جديد من SObjectizer ، واجهتني مهمة التحكم في تصرفات المطور في وقت الترجمة. خلاصة القول هي أنه في السابق يمكن للمبرمج إجراء مكالمات من النموذج:
receive(from(ch).empty_timeout(150ms), ...); receive(from(ch).handle_n(2).no_wait_on_empty(), ...); receive(from(ch).empty_timeout(2s).extract_n(20).stop_on(...), ...); receive(from(ch).no_wait_on_empty().stop_on(...), ...);
تتطلب عملية التلقي () مجموعة من المعلمات ، والتي تم استخدام سلسلة من الأساليب ، مثل تلك الموضحة أعلاه from(ch).empty_timeout(150ms)
أو from(ch).handle_n(2).no_wait_on_empty()
. في نفس الوقت ، كان استدعاء أساليب handle_n () / extract_n () ، والتي تحد من عدد الرسائل التي سيتم استخراجها / معالجتها ، اختياريًا. لذلك ، كل السلاسل الموضحة أعلاه كانت صحيحة.
ولكن في الإصدار الجديد ، كان مطلوبًا إجبار المستخدم على الإشارة صراحة إلى عدد الرسائل التي يجب استخراجها و / أو معالجتها. أي أصبحت سلسلة النموذج from(ch).empty_timeout(150ms)
الآن غير صحيحة. يجب استبداله بـ from(ch).handle_all().empty_timeout(150ms)
.
وأردت إجراء ذلك حتى يتغلب المترجم على المبرمج يدويًا إذا نسي المبرمج إجراء مكالمة إلى handle_all () أو handle_n () أو extract_n ().
يمكن C ++ مساعدة في هذا؟
نعم. وإذا كان شخص ما مهتمًا بالتحديد ، فأنت مرحب بك تحت قطة.
يوجد أكثر من دالة استقبال ()
تم عرض وظيفة الاستقبال () أعلاه ، حيث تم ضبط المعلمات الخاصة بها باستخدام سلسلة من المكالمات (المعروفة أيضًا باسم نمط البناء ). ولكن كانت هناك أيضًا وظيفة select () ، والتي تلقت نفس مجموعة المعلمات تقريبًا:
select(from_all().empty_timeout(150ms), case_(...), case_(...), ...); select(from_all().handle_n(2).no_wait_on_empty(), case_(...), case_(...), ...); select(from_all().empty_timeout(2s).extract_n(20).stop_on(...), case_(...), case_(...), ...); select(from_all().no_wait_on_empty().stop_on(...), case_(...), case_(...), ...);
وفقًا لذلك ، أردت الحصول على حل واحد يناسب كل من تحديد () وتلقي (). علاوة على ذلك ، تم تحديد معلمات select () وتلقي () نفسها بالفعل في التعليمات البرمجية لتجنب النسخ واللصق. ولكن هذا سوف يناقش أدناه.
الحلول الممكنة
لذلك ، فإن المهمة هي أن يقوم المستخدم باستدعاء handle_all () أو handle_n () أو extract_n () دون فشل.
من حيث المبدأ ، يمكن تحقيق ذلك دون اللجوء إلى أي قرارات معقدة. على سبيل المثال ، يمكنك إدخال وسيطة إضافية لتحديد () وتلقي ():
receive(handle_all(), from(ch).empty_timeout(150ms), ...); select(handle_n(20), from_all().no_wait_on_empty(), ...);
أو سيكون من الممكن إجبار المستخدم على إجراء مكالمة الاستقبال () / select () بشكل مختلف:
receive(handle_all(from(ch).empty_timeout(150ms)), ...); select(handle_n(20, from_all().no_wait_on_empty()), ...);
ولكن المشكلة هنا هي أنه عند التبديل إلى إصدار جديد من SObjectizer ، سيتعين على المستخدم إعادة رمزه. حتى لو كان الكود ، من حيث المبدأ ، لا يتطلب إعادة صياغة. قل ، في هذه الحالة:
receive(from(ch).handle_n(2).no_wait_on_empty(), ...); select(from_all().empty_timeout(2s).extract_n(20).stop_on(...), case_(...), case_(...), ...);
وهذه ، في رأيي ، مشكلة خطيرة للغاية. مما يجعلك تبحث عن طريقة أخرى. وسيتم وصف هذه الطريقة أدناه.
فأين يأتي CRTP؟
عنوان المقالة المذكورة CRTP. إنه أيضًا نموذج قالب متكرر بفضول (يمكن لأولئك الذين يرغبون في التعرف على هذا الأسلوب المثير للاهتمام ، لكن المتسامح قليلاً مع الدماغ ، أن يبدأوا بهذه السلسلة من المنشورات على مدونة Fluent C ++).
تم ذكر CRTP لأنه من خلال CRTP ، قمنا بتنفيذ العمل باستخدام معلمات الدالة rece () وحدد (). نظرًا لأن نصيب الأسد من معلمات الاستلام () وحدد () كان هو نفسه ، فقد استخدم الكود شيئًا مثل هذا:
template<typename Derived> class bulk_processing_params_t { ...;
لماذا CRTP هنا على الإطلاق؟
كان علينا استخدام CRTP هنا حتى تتمكن طرق setter التي تم تعريفها في الفئة الأساسية من إرجاع مرجع ليس إلى النوع الأساسي ، ولكن إلى النوع المشتق.
أي إذا لم يتم استخدام CRTP ، ولكن الميراث العادي ، عندئذٍ يمكننا فقط كتابة مثل هذا:
class bulk_processing_params_t { public:
لكن هذه الآلية البدائية لن تسمح لنا باستخدام نفس نمط البناء ، لأنه:
receive_processing_params_t{}.handle_n(20).receive_payload(0)
لا جمعت. ستُرجع الطريقة handle_n () مرجعًا إلى bulk_processing_params_t ، وهناك لم يتم تحديد طريقة rece_payload () بعد.
ولكن مع CRTP ليس لدينا أي مشاكل مع نمط البناء.
القرار النهائي
الحل النهائي هو أن تصبح الأنواع النهائية ، مثل rece_processing_params_t و select_processing_params_t ، أنواع القوالب نفسها. بحيث يتم تحديد معلماتهم باستخدام عدد قياسي من النموذج التالي:
enum class msg_count_status_t { undefined, defined };
وبالتالي يمكن تحويل النوع الأخير من T <msg_count_status_t :: undefined> إلى T <msg_count_status_t ::efined>.
سيسمح هذا ، على سبيل المثال ، في الدالة rece () بتلقي rece_processing_params_t والتحقق من قيمة الحالة في وقت comp. شيء مثل:
template< msg_count_status_t Msg_Count_Status, typename... Handlers > inline mchain_receive_result_t receive( const mchain_receive_params_t<Msg_Count_Status> & params, Handlers &&... handlers ) { static_assert( Msg_Count_Status == msg_count_status_t::defined, "message count to be processed/extracted should be defined " "by using handle_all()/handle_n()/extract_n() methods" );
بشكل عام ، كل شيء بسيط ، كالعادة: خذ وتفعل ؛)
وصف القرار المتخذ
دعونا نلقي نظرة على مثال بسيط ، منفصل عن تفاصيل SObjectizer ، كما يبدو.
لذلك ، لدينا بالفعل نوع يحدد ما إذا كان قد تم تعيين الحد الأقصى لعدد الرسائل أم لا:
enum class msg_count_status_t { undefined, defined };
بعد ذلك ، نحتاج إلى هيكل يتم فيه تخزين جميع المعلمات الشائعة:
struct basic_data_t { int to_extract_{}; int to_handle_{}; int common_payload_{}; };
بشكل عام ، لا يهم ما هي محتويات basic_data_t. على سبيل المثال ، مجموعة الحقول الدنيا الموضحة أعلاه مناسبة.
فيما يتعلق بـ basic_data_t ، من المهم بالنسبة لعمليات محددة (سواء أكان استلام () أو حدد () أو أي شيء آخر) ، سيتم إنشاء نوعه الملموس الذي يرث basic_data_t. على سبيل المثال ، بالنسبة للتلقي () في مثالنا المجرد ، سيكون هذا هو الهيكل التالي:
struct receive_specific_data_t final : public basic_data_t { int receive_payload_{}; receive_specific_data_t() = default; receive_specific_data_t(int v) : receive_payload_{v} {} };
نحن نفترض أن بنية basic_data_t وأحفادها لا تسبب صعوبات. لذلك ، ننتقل إلى الأجزاء الأكثر تعقيدًا من الحل.
نحتاج الآن إلى برنامج التفاف حول basic_data_t ، والذي سيوفر طرقًا جيدة. ستكون فئة قالب من النموذج التالي:
template<typename Basic_Data> class basic_data_holder_t { private : Basic_Data data_; protected : void set_to_extract(int v) { data_.to_extract_ = v; } void set_to_handle(int v) { data_.to_handle_ = v; } void set_common_payload(int v) { data_.common_payload_ = v; } const auto & data() const { return data_; } public : basic_data_holder_t() = default; basic_data_holder_t(Basic_Data data) : data_{std::move(data)} {} int to_extract() const { return data_.to_extract_; } int to_handle() const { return data_.to_handle_; } int common_payload() const { return data_.common_payload_; } };
هذه الفئة عبارة عن قواعد نحاسية بحيث يمكن أن تحتوي على أي وراثة من basic_data_t ، على الرغم من أنها تنفذ أساليب getter فقط لتلك الحقول الموجودة في basic_data_t.
قبل الانتقال إلى الأجزاء الأكثر تعقيدًا من الحل ، يجب الانتباه إلى طريقة البيانات () في basic_data_holder_t. هذه طريقة مهمة وسنواجهها لاحقًا.
الآن يمكننا الانتقال إلى فئة القوالب الرئيسية ، والتي يمكن أن تبدو مخيفة جدًا للأشخاص الذين ليسوا مخلصين للغاية لـ C ++ الحديثة:
template<typename Data, typename Derived> class basic_params_t : public basic_data_holder_t<Data> { using base_type = basic_data_holder_t<Data>; public : using actual_type = Derived; using data_type = Data; protected : actual_type & self_reference() { return static_cast<actual_type &>(*this); } decltype(auto) clone_as_defined() { return self_reference().template clone_if_necessary< msg_count_status_t::defined >(); } public : basic_params_t() = default; basic_params_t(data_type data) : base_type{std::move(data)} {} decltype(auto) handle_all() { this->set_to_handle(0); return clone_as_defined(); } decltype(auto) handle_n(int v) { this->set_to_handle(v); return clone_as_defined(); } decltype(auto) extract_n(int v) { this->set_to_extract(v); return clone_as_defined(); } actual_type & common_payload(int v) { this->set_common_payload(v); return self_reference(); } using base_type::common_payload; };
هذا basic_params_t هو قالب CRTP الرئيسي. الآن فقط هو معلمة من قبل اثنين من المعلمات.
المعلمة الأولى هي نوع البيانات التي يجب تضمينها في الداخل. على سبيل المثال ، rece_specific_data_t أو select_specific_data_t.
المعلمة الثانية هي نوع الخلف المألوف لـ CRTP. يتم استخدامه في طريقة self_reference () للحصول على مرجع لنوع مشتق.
النقطة الأساسية في تنفيذ القالب basic_params_t هي طريقة clone_as_defined (). تتوقع هذه الطريقة أن يقوم الوريث بتطبيق طريقة clone_if_necessary (). وهذا clone_if_necessary () مصمم فقط لتحويل الكائن T <msg_count_status_t :: undefined> إلى الكائن T <msg_count_status_t :: defined>. ويتم بدء مثل هذا التحويل في أساليب setter handle_all () و handle_n () و extract_n ().
علاوة على ذلك ، يمكنك الانتباه إلى حقيقة أن clone_as_defined () و handle_all () و handle_n () و extract_n () تحدد نوع قيمة الإرجاع الخاصة بهم كـ dectype (تلقائي). هذه خدعة أخرى سنتحدث عنها قريبًا.
الآن يمكننا أن ننظر بالفعل في واحدة من الأنواع النهائية ، والتي تم تصور كل هذا:
template< msg_count_status_t Msg_Count_Status > class receive_specific_params_t final : public basic_params_t< receive_specific_data_t, receive_specific_params_t<Msg_Count_Status> > { using base_type = basic_params_t< receive_specific_data_t, receive_specific_params_t<Msg_Count_Status> >; public : template<msg_count_status_t New_Msg_Count_Status> std::enable_if_t< New_Msg_Count_Status != Msg_Count_Status, receive_specific_params_t<New_Msg_Count_Status> > clone_if_necessary() const { return { this->data() }; } template<msg_count_status_t New_Msg_Count_Status> std::enable_if_t< New_Msg_Count_Status == Msg_Count_Status, receive_specific_params_t& > clone_if_necessary() { return *this; } receive_specific_params_t(int receive_payload) : base_type{ typename base_type::data_type{receive_payload} } {} receive_specific_params_t(typename base_type::data_type data) : base_type{ std::move(data) } {} int receive_payload() const { return this->data().receive_payload_; } };
أول شيء يجب الانتباه إليه هنا هو المنشئ ، الذي يأخذ base_type :: data_type. باستخدام هذا المُنشئ ، يتم نقل قيم المعلمات الحالية أثناء التحويل من T <msg_count_status_t :: undefined> إلى T <msg_count_status_t ::efined>.
على العموم ، فإن هذا Rece_specific_params_t شيء من هذا القبيل:
template<typename V, int K> class holder_t { V v_; public: holder_t() = default; holder_t(V v) : v_{std::move(v)} {} const V & value() const { return v_; } }; holder_t<std::string, 0> v1{"Hello!"}; holder_t<std::string, 1> v2; v2 = v1;
وفقط المُنشئ المذكور rece_specific_params_t يسمح بتهيئة rece_specific_params_t <msg_count_status_t :: defined> بقيم من rece_specific_params_t <msg_count_status_t :: undefined>.
الشيء المهم الثاني في rece_specific_params_t هو طريقتي clone_if_necessary ().
لماذا هناك اثنين؟ وماذا يعني كل هذا السحر SFINAE-vskaya في تعريفهم؟
تم إجراء طريقتين clone_if_necessary () من أجل تجنب التحولات غير الضرورية. افترض أن مبرمجًا يدعى أسلوب handle_n () وتلقى بالفعل rece_specific_params_t <msg_count_status_t :: defined>. وبعد ذلك دعا extract_n (). هذا مسموح به ، وتعيين handle_n () و extract_n () قيودًا مختلفة قليلاً. يجب أن تعطينا الدعوة إلى extract_n () أيضًا rece_specific_params_t <msg_count_status_t ::efined>. لكن لدينا بالفعل واحدة. فلماذا لا إعادة استخدام واحد موجود؟
لهذا السبب هناك طريقتان clone_if_necessary () هنا. الأول سيعمل عندما تكون هناك حاجة حقيقية للتحول:
template<msg_count_status_t New_Msg_Count_Status> std::enable_if_t< New_Msg_Count_Status != Msg_Count_Status, receive_specific_params_t<New_Msg_Count_Status> > clone_if_necessary() const { return { this->data() }; }
سيقوم المترجم بتحديده ، على سبيل المثال ، عندما تتغير الحالة من غير معرف إلى معرف. وهذه الطريقة ستعود لكائن جديد. ونعم ، في تطبيق هذه الطريقة ، ننتبه إلى مكالمة البيانات () ، والتي تم تحديدها بالفعل في basic_data_holder_t.
الطريقة الثانية:
template<msg_count_status_t New_Msg_Count_Status> std::enable_if_t< New_Msg_Count_Status == Msg_Count_Status, receive_specific_params_t& > clone_if_necessary() { return *this; }
سيتم استدعاء عندما لا يكون من الضروري تغيير الوضع. وتقوم هذه الطريقة بإرجاع مرجع إلى كائن موجود.
الآن يجب أن يصبح من الواضح لماذا في basic_params_t لعدد من الطرق تم تعريف نوع الإرجاع كـ Dectype (تلقائي). بعد كل شيء ، تعتمد هذه الطرق على إصدار معين من clone_if_necessary () سيتم استدعاؤه في النوع المشتق ، ولا يمكن إرجاع كائن أو رابط ... لا يمكنك التنبؤ به مقدمًا. وهنا Dectype (السيارات) يأتي لانقاذ.
إخلاء صغير
كان المثال البسيط الموصوف موجهًا إلى العرض الأبسط والأكثر قابلية للفهم للحل المختار. لذلك ، فإنه لا يحتوي على أشياء واضحة تمامًا والتي يجب تضمينها في التعليمات البرمجية.
على سبيل المثال ، ترجع طريقة basic_data_holder_t :: data () مرجعًا ثابتًا إلى البيانات. يؤدي هذا إلى نسخ قيم المعلمات أثناء تحويل T <msg_count_status_t :: undefined> إلى T <msg_count_status_t :: defined>. إذا كانت عملية نسخ المعلمات عملية باهظة الثمن ، فيجب أن تكون في حيرة من أمر تحريك الدلالات ويمكن أن يكون لطريقة البيانات () النموذج التالي:
auto data() { return std::move(data_); }
الآن أيضًا ، في كل نوع نهائي (مثل rece_specific_params_t و select_specific_params_t) ، يجب عليك تضمين تطبيقات الأساليب clone_if_necessary. أي في هذا المكان ما زلنا نستخدم معجون النسخ. ربما يجب أن يكون هناك أيضًا شيء ما يجب التوصل إليه لتجنب تكرار نفس النوع من التعليمات البرمجية.
حسنًا ، ونعم ، لا يتم وضع noexcept في التعليمات البرمجية من أجل تقليل "(أحمال) بناء الجملة".
هذا كل شيء
يمكن العثور على الكود المصدري للمثال البسيط الذي تمت مناقشته هنا . ويمكنك اللعب في برنامج التحويل البرمجي عبر الإنترنت ، على سبيل المثال ، هنا (يمكنك التعليق على المكالمة إلى handle_all () على السطر 163 ومعرفة ما يحدث).
لا أريد أن أقول إن الطريقة التي طبقتها هي الطريقة الصحيحة الوحيدة. ولكن ، أولاً ، رأيت بديلاً إلا في النسخ واللصق. وثانياً ، لم يكن الأمر صعبًا على الإطلاق ، ولحسن الحظ لم يستغرق الأمر الكثير من الوقت. لكن اللكمات المترجم ساعدت كثيرا على الفور ، حيث تم تكييف الاختبارات القديمة والأمثلة مع الميزات الجديدة لأحدث إصدار من SObjectizer.
لذلك ، بالنسبة لي ، أكدت C ++ مرة أخرى أنها معقدة. لكن ليس هكذا فقط ، ولكن لإعطاء المزيد من الفرص للمطور. حسنًا ، لن أتفاجأ إذا كان يمكن الحصول على كل هذا في الإصدار C ++ الحديث بطريقة أبسط مما فعلت.
PS. إذا اتبع أحد القراء برنامج SObjectizer ، فيمكنني أن أقول إن الإصدار الجديد 5.6 ، الذي تم فيه انتهاك التوافق مع الفرع 5.5 بشكل كبير ، قد تنفس بالفعل قليلاً جدًا. يمكنك العثور عليه على BitBucket . لا يزال الإصدار بعيدًا ، لكن SObjectizer-5.6 هو بالفعل ما كان من المفترض أن يكون. يمكنك أن تأخذ ، ومحاولة وتبادل انطباعاتك.