... كيفية ملء فئة القالب بمحتويات مختلفة حسب قيم معلمات القالب؟
ذات مرة ، لبعض الوقت ، بدأت لغة D لتصبح "C ++ الصحيحة" ، مع مراعاة الخبرة المكتسبة في C ++. بمرور الوقت ، أصبح D لغة أقل تعقيدًا وأكثر تعبيرًا من لغة C ++. وبالفعل بدأت C ++ بالتجسس على D. على سبيل المثال ، ظهرت في C ++ 17 if constexpr
، في رأيي ، عبارة عن استعارة مباشرة من D ، كان النموذج الأولي لـ D-shny ثابت إذا .
لسوء الحظ ، if constexpr
لدى if constexpr
في C ++ نفس القوة مثل static if
في D. هناك أسباب لذلك ، ولكن لا تزال هناك حالات لا يمكنك أن تندم فيها إلا if constexpr
في C ++ لا يسمح لك بالتحكم في محتوى C + + الطبقة. أود التحدث عن إحدى هذه الحالات.
سنتحدث عن كيفية إنشاء فئة قالب ، والتي قد تتغير محتوياتها (أي تكوين الأساليب ومنطق بعض الأساليب) اعتمادًا على المعلمات التي تم تمريرها إلى فئة القالب هذه. مثال مأخوذ من واقع الحياة ، من تجربة تطوير نسخة جديدة من SObjectizer .
المهمة التي يتعين حلها
يجب إنشاء إصدار ذكي من "المؤشر الذكي" لتخزين كائنات الرسائل. بحيث يمكنك كتابة شيء مثل:
message_holder_t<my_message> msg{ new my_message{...} }; send(target, msg); send(another_target, msg);
تكمن حيلة فئة message_holder_t
وجود ثلاثة عوامل مهمة يجب مراعاتها.
ما هو نوع الرسالة الموروثة من؟
تنقسم أنواع الرسائل التي تحدد معلمة message_holder_t
إلى مجموعتين. المجموعة الأولى هي الرسائل التي ترث من النوع الأساسي الخاص message_t
. على سبيل المثال:
struct so5_message final : public so_5::message_t { int a_; std::string b_; std::chrono::milliseconds c_; so5_message(int a, std::string b, std::chrono::milliseconds c) : a_{a}, b_{std::move(b)}, c_{c} {} };
في هذه الحالة ، يجب أن يحتوي message_holder_t داخل نفسه فقط على مؤشر إلى كائن من هذا النوع. يجب أن يتم إرجاع نفس المؤشر في أساليب getter. أي بالنسبة إلى ولي العهد من message_t
يجب أن يكون message_t
شيء مثل:
template<typename M> class message_holder_t { intrusive_ptr_t<M> m_msg; public: ... const M * get() const noexcept { return m_msg.get(); } };
المجموعة الثانية هي رسائل أنواع المستخدمين التعسفية التي لم يتم توريثها من message_t
. على سبيل المثال:
struct user_message final { int a_; std::string b_; std::chrono::milliseconds c_; user_message(int a, std::string b, std::chrono::milliseconds c) : a_{a}, b_{std::move(b)}, c_{c} {} };
لا يتم إرسال مثيلات هذه الأنواع في SObjectizer من تلقاء نفسها ، ولكن يتم تضمينها في برنامج تغليف خاص ، user_type_message_t<M>
، والذي تم توارثه بالفعل من message_t
. لذلك ، بالنسبة لهذه الأنواع ، يجب أن تحتوي message_holder_t
على مؤشر إلى user_type_message_t<M>
بداخلها ، ويجب أن تُرجع أساليب getter مؤشرًا إلى M:
template<typename M> class message_holder_t { intrusive_ptr_t<user_type_message_t<M>> m_msg; public: ... const M * get() const noexcept { return std::addressof(m_msg->m_payload); } };
حصانة أو قابلية الرسائل
العامل الثاني هو تقسيم الرسائل إلى ثابت وغير قابل للتغيير. إذا كانت الرسالة غير قابلة للتغيير (وهي افتراضيًا غير قابلة للتغيير) ، فيجب على أساليب getter إرجاع مؤشر ثابت إلى الرسالة. وإذا كان قابلاً للتغيير ، فيجب أن تُرجع getters مؤشرًا غير ثابت. أي يجب أن يكون شيء مثل:
message_holder_t<so5_message> msg1{...};
Shared_ptr vs unique_ptr
العامل الثالث هو منطق سلوك message_holder_t
كمؤشر ذكي. بمجرد أن يتصرف مثل std::shared_ptr
، أي يمكنك مطالبة أصحاب رسائل متعددة بالإشارة إلى مثيل الرسالة نفسه. وبمجرد أن يتصرف مثل std::unique_ptr
، أي يمكن لمثيل واحد فقط من أصحاب الرسالة الرجوع إلى مثيل الرسالة.
بشكل افتراضي ، يجب أن يعتمد سلوك message_holder_t
على قابلية / عدم قابلية الرسالة للتغيير. أي مع الرسائل غير message_holder_t
للتغيير ، يجب أن يتصرف message_holder_t
مثل std::shared_ptr
، ومع std::unique_ptr
للتغيير مثل std::unique_ptr
:
message_holder_t<so5_message> msg1{...}; message_holder_t<so5_message> msg2 = msg;
لكن الحياة شيء معقد ، لذا عليك أيضًا أن تكون قادرًا على ضبط سلوك message_holder_t
يدويًا. بحيث يمكنك إنشاء message_holder لرسالة ثابتة تتصرف مثل unique_ptr. وبهذا يمكنك إنشاء message_holder لرسالة قابلة للتغيير تتصرف مثل shared_ptr:
using unique_so5_message = so_5::message_holder_t< so5_message, so_5::message_ownership_t::unique>; unique_so5_message msg1{...}; unique_so5_message msg2 = msg1;
وفقًا لذلك ، عندما تعمل message_holder_t
مثل shared_ptr ، فيجب أن يكون لديها المجموعة المعتادة من المنشئات ومشغلي التعيين: النسخ والتنقل. بالإضافة إلى ذلك ، يجب أن يكون هناك طريقة ثابتة make_reference
، والتي تُرجع نسخة من المؤشر المخزن داخل message_holder_t
.
ولكن عندما تعمل message_holder_t
مثل unique_ptr ، عندئذٍ يجب حظر مُنشئ النسخة ومشغل النسخ لها. ويجب أن تأخذ طريقة make_reference
المؤشر من كائن message_holder_t
: بعد استدعاء make_reference
يجب أن يبقى message_holder_t
الأصلي فارغًا.
لذلك ، تحتاج إلى إنشاء فئة قالب:
template< typename M, message_ownership_t Ownership = message_ownership_t::autodetected> class message_holder_t {...};
في ما يلي:
- يجب تخزين
intrusive_ptr_t<M>
أو intrusive_ptr<user_type_message_t<M>>
الداخل ، اعتمادًا على ما إذا كانت M موروثة من message_t
؛ - يجب أن تُرجع طرق getter إما
const M*
أو M*
اعتمادًا على قابلية / عدم قابلية الرسالة للتغيير ؛ - يجب أن يكون هناك إما مجموعة كاملة من المنشئات ومشغلي النسخ / النقل ، أو فقط مُنشئ ومشغل نقل ؛
- يجب أن تقوم طريقة
make_reference()
إما بإرجاع نسخة من intrusive_ptr المخزنة ، أو يجب أن تأخذ قيمة intrusive_ptr وتترك message_holder_t
الأصلية فارغة. في الحالة الأولى ، يجب أن يكون make_reference()
ثابتًا ، في الطريقة الثانية - غير الثابتة.
يتم تحديد آخر عنصرين من القائمة بواسطة معلمة الملكية (وكذلك قابلية تحويل الرسالة في حالة استخدام autodetected
للملكية).
كيف تقرر
في هذا القسم ، سننظر في جميع المكونات التي تشكل الحل النهائي. حسنا ، الحل الناتج نفسه. سيتم عرض أجزاء الكود التي تم مسحها من كل تفاصيل التشتيت. إذا كان شخص ما مهتمًا بالرمز الحقيقي ، فيمكنك مشاهدته هنا .
تنصل
الحل الموضح أدناه لا يدعي أنه جميل أو مثالي أو نموذج يحتذى به. تم العثور عليها وتنفيذها واختبارها وتوثيقها في وقت قصير ، تحت ضغط من المواعيد النهائية. ربما إذا كان هناك مزيد من الوقت ، وكان أكثر منخرط في البحث عن حل شاب حساسة ودراية في المطور C ++ الحديث ، فإنه سيتحول إلى أن يكون أكثر إحكاما وأكثر بساطة وأكثر قابلية للفهم. ولكن ، كما اتضح ، حدث ذلك ... "لا تطلقوا النار على عازف البيانو" ، بشكل عام.
سلسلة من الخطوات والسحر قالب جاهز
لذلك ، نحن بحاجة إلى فصل مع عدة مجموعات من الأساليب. يجب أن تأتي محتويات هذه المجموعات من مكان ما. من اين
في D ، يمكننا استخدام static if
وتعريف أجزاء مختلفة من الفصل تبعا لظروف مختلفة. في بعض روبي ، يمكننا دمج الأساليب في فصلنا باستخدام طريقة التضمين . لكننا في C ++ ، حيث إمكانياتنا محدودة للغاية حتى الآن: يمكننا إما تحديد أسلوب / سمة مباشرة داخل الفصل ، أو يمكننا أن نرث الطريقة / السمة من فئة أساسية معينة.
لا يمكننا تحديد أساليب / سمات مختلفة داخل الفصل اعتمادًا على بعض الشروط ، لأن C ++ if constexpr
ليس static if
. وبالتالي ، يبقى الميراث فقط.
محدث. كما هو مقترح في التعليقات ، يجب أن أتحدث بعناية أكبر هنا. نظرًا لأن C ++ لديه SFINAE ، يمكننا تمكين / تعطيل رؤية الطرق الفردية في الفصل الدراسي عبر SFINAE (أي تحقيق تأثير مماثل للتأثير static if
). ولكن هذا النهج لديه اثنين من أوجه القصور الخطيرة ، في رأيي. أولاً ، إذا لم تكن هذه الطرق من 1-2 إلى 3 ، ولكن من 4 إلى 5 أو أكثر ، فإن تنسيق كل منها باستخدام SFINAE يكون شاقًا ، وهذا يؤثر على قابلية قراءة الكود. ثانياً ، لا يساعدنا SFINAE في إضافة / إزالة سمات الصف (الحقول).
في C ++ ، يمكننا تحديد العديد من الفئات الأساسية التي سنرث من خلالها message_holder_t
. وسيتم بالفعل اختيار فئة أساسية واحدة أو أخرى وفقًا لقيم معلمات القالب ، باستخدام std :: conditional .
لكن الحيلة هي أننا لا نحتاج فقط إلى مجموعة من الطبقات الأساسية ، ولكن إلى سلسلة صغيرة من الميراث. في بدايتها ، سيكون هناك فصل يحدد الوظيفة العامة المطلوبة في أي حال. التالي سيكون الفئات الأساسية التي ستحدد منطق سلوك "المؤشر الذكي". وبعد ذلك سيكون هناك فئة تحدد الحروف اللازمة. بهذا الترتيب سننظر في الطبقات المنفذة.
يتم تبسيط مهمتنا من خلال حقيقة أن SObjectizer يحتوي بالفعل على قالب قوالب جاهز يحدد ما إذا كانت الرسالة موروثة من message_t ، وكذلك وسيلة للتحقق من قابلية نقل الرسائل . لذلك ، في التنفيذ ، سنستخدم ببساطة هذا السحر الجاهزة ولن نتعمق في تفاصيل عمله.
قاعدة تخزين مؤشر مشتركة
لنبدأ بنوع أساسي شائع يخزن intrusive_ptr المطابق ، ويوفر أيضًا مجموعة مشتركة من الطرق التي يحتاجها أي من تطبيقات message_holder_t
:
template< typename Payload, typename Envelope > class basic_message_holder_impl_t { protected : intrusive_ptr_t< Envelope > m_msg; public : using payload_type = Payload; using envelope_type = Envelope; basic_message_holder_impl_t() noexcept = default; basic_message_holder_impl_t( intrusive_ptr_t< Envelope > msg ) noexcept : m_msg{ std::move(msg) } {} void reset() noexcept { m_msg.reset(); } [[nodiscard]] bool empty() const noexcept { return static_cast<bool>( m_msg ); } [[nodiscard]] operator bool() const noexcept { return !this->empty(); } [[nodiscard]] bool operator!() const noexcept { return this->empty(); } };
فئة القالب هذه لها معلمتان. الأول ، Payload ، يحدد النوع الذي يجب أن تستخدمه طرق getter. بينما الثاني ، Envelope ، يعين نوع intrusive_ptr. في حالة توارث نوع الرسالة من message_t
لكل من هذه المعلمات نفس القيمة. ولكن إذا لم يتم توريث الرسالة من message_t
، فسيتم استخدام نوع الرسالة كـ Payload ، وسيكون user_type_message_t<Payload>
بمثابة Envelope.
أعتقد أن محتوى هذا الفصل لا يثير الأسئلة بشكل أساسي. ولكن ينبغي ملاحظة شيئين بشكل منفصل.
أولاً ، المؤشر نفسه ، أي يتم تعريف سمة m_msg في القسم المحمي بحيث يمكن لوراثة الفئات الوصول إليها.
ثانياً ، بالنسبة لهذه الفئة ، يقوم المترجم نفسه بتوليد جميع المنشئات اللازمة ومشغلي النسخ / النقل. وعلى مستوى هذه الفئة ، نحن لا نحظر أي شيء حتى الآن.
قواعد منفصلة لسلوك Shared_ptr و unique_ptr
لذلك ، لدينا فئة يخزن مؤشر لرسالة. الآن يمكننا تحديد ورثته ، الذين سيتصرفون إما كـ Shared_ptr أو unique_ptr.
دعنا نبدأ مع حالة Shared_ptr من السلوك ، لأن هنا هو أقل رمز:
template< typename Payload, typename Envelope > class shared_message_holder_impl_t : public basic_message_holder_impl_t<Payload, Envelope> { using direct_base_type = basic_message_holder_impl_t<Payload, Envelope>; public : using direct_base_type::direct_base_type; [[nodiscard]] intrusive_ptr_t< Envelope > make_reference() const noexcept { return this->m_msg; } };
لا يوجد شيء معقد: ترث من basic_message_holder_impl_t
، ورث كل basic_message_holder_impl_t
بسيطًا وغير مدمر لـ make_reference()
.
في حالة سلوك unique_ptr ، يكون الرمز أكبر ، على الرغم من عدم وجود شيء معقد فيه:
template< typename Payload, typename Envelope > class unique_message_holder_impl_t : public basic_message_holder_impl_t<Payload, Envelope> { using direct_base_type = basic_message_holder_impl_t<Payload, Envelope>; public : using direct_base_type::direct_base_type; unique_message_holder_impl_t( const unique_message_holder_impl_t & ) = delete; unique_message_holder_impl_t( unique_message_holder_impl_t && ) = default; unique_message_holder_impl_t & operator=( const unique_message_holder_impl_t & ) = delete; unique_message_holder_impl_t & operator=( unique_message_holder_impl_t && ) = default; [[nodiscard]] intrusive_ptr_t< Envelope > make_reference() noexcept { return { std::move(this->m_msg) }; } };
مرة أخرى ، نرث من basic_message_holder_impl_t
التي نحتاجها منه (هذا هو المنشئ الافتراضي basic_message_holder_impl_t
التهيئة). ولكن في الوقت نفسه ، نحدد المنشئات ومشغلي النسخ / النقل وفقًا للمنطق الفريد: نحن نحظر النسخ ، وننفذ هذه الخطوة.
لدينا أيضًا طريقة make_reference()
مدمرة make_reference()
.
هذا كل شيء ، في الواقع. يبقى فقط لتحقيق الاختيار بين هاتين الفئتين الأساسيتين ...
الاختيار بين Shared_ptr وسلوك unique_ptr
للاختيار بين سلوك Shared_ptr و unique_ptr ، تحتاج إلى ملف التعريف التالي (ملف تعريف لأنه "يعمل" مع أنواع في وقت الترجمة):
template< typename Msg, message_ownership_t Ownership > struct impl_selector { static_assert( !is_signal<Msg>::value, "Signals can't be used with message_holder" ); using P = typename message_payload_type< Msg >::payload_type; using E = typename message_payload_type< Msg >::envelope_type; using type = std::conditional_t< message_ownership_t::autodetected == Ownership, std::conditional_t< message_mutability_t::immutable_message == message_mutability_traits<Msg>::mutability, shared_message_holder_impl_t<P, E>, unique_message_holder_impl_t<P, E> >, std::conditional_t< message_ownership_t::shared == Ownership, shared_message_holder_impl_t<P, E>, unique_message_holder_impl_t<P, E> > >; };
يقبل ملف التعريف هذا كلا المعلمتين من قائمة معلمات message_holder_t
، ونتيجة لذلك (أي ، تعريف type
المتداخل) ، "يُرجع" النوع الذي يجب أن يُورث منه. أي إما shared_message_holder_impl_t
أو unique_message_holder_impl_t
.
داخل تعريف impl_selector
يمكنك مشاهدة آثار السحر المذكورة أعلاه ، والتي لم نذهب إلى: message_payload_type<Msg>::payload_type
، message_payload_type<Msg>::envelope_type
و message_mutability_traits<Msg>::mutability
. message_mutability_traits<Msg>::mutability
.
ومن أجل استخدام impl_selector
كان أسهل ، فسوف نحدد اسمًا أقصر له:
template< typename Msg, message_ownership_t Ownership > using impl_selector_t = typename impl_selector<Msg, Ownership>::type;
قاعدة للالعاب
لذلك ، لدينا بالفعل الفرصة لتحديد قاعدة تحتوي على مؤشر وتعريف سلوك "مؤشر ذكي". الآن نحن بحاجة إلى توفير هذه القاعدة مع أساليب getter. لماذا نحتاج إلى فصل واحد بسيط:
template< typename Base, typename Return_Type > class msg_accessors_t : public Base { public : using Base::Base; [[nodiscard]] Return_Type * get() const noexcept { return get_ptr( this->m_msg ); } [[nodiscard]] Return_Type & operator * () const noexcept { return *get(); } [[nodiscard]] Return_Type * operator->() const noexcept { return get(); } };
هذه فئة قالب تعتمد على معلمتين ، لكن معناها مختلف تمامًا. ستكون المعلمة Base نتيجة impl_selector
impl_selector الموضحة أعلاه. أي كمعلمة Base ، يتم تعيين الفئة الأساسية التي ترث منها.
من المهم الإشارة إلى أنه إذا كان الميراث يأتي من unique_message_holder_impl_t
، والذي يُمنع المُنشئ ومشغل النسخ له ، فلن يتمكن المترجم من إنشاء مُنشئ ونسخ عامل التشغيل msg_accessors_t
. وهو ما نحتاجه
سيكون نوع الرسالة ، المؤشر / الارتباط الذي سيتم إرجاعه بواسطة getters ، بمثابة المعلمة Return_Type. الحيلة هي أنه بالنسبة لرسالة ثابتة من النوع Msg
سيتم تعيين المعلمة Return_Type إلى const Msg
. بينما بالنسبة لرسالة قابلة للتغيير من النوع Msg
المعلمة Return_Type سيكون لها القيمة Msg
. وبالتالي ، فإن الأسلوب get()
سيعود const Msg*
للرسائل غير القابلة للتغيير ، وفقط Msg*
للرسائل القابلة للتغيير.
باستخدام get_ptr()
الدالة الحرة get_ptr()
نحل مشكلة العمل مع الرسائل التي لم يتم توارثها من message_t
:
template< typename M > M * get_ptr( const intrusive_ptr_t<M> & msg ) noexcept { return msg.get(); } template< typename M > M * get_ptr( const intrusive_ptr_t< user_type_message_t<M> > & msg ) noexcept { return std::addressof(msg->m_payload); }
أي إذا لم يتم توريث الرسالة من message_t
وتخزينها على أنها user_type_message_t<Msg>
، فسيتم استدعاء التحميل الزائد الثاني. وإذا كان موروثا ، ثم الزائد الأول.
اختيار قاعدة محددة للألعاب
لذلك ، يتطلب القالب msg_accessors_t
معلمتين. يتم حساب الأول عن طريق impl_selector
metafunction. ولكن من أجل تكوين نوع أساسي محدد من msg_accessors_t
، نحتاج إلى تحديد قيمة المعلمة الثانية. ملف تعريف آخر مخصص لهذا:
template< message_mutability_t Mutability, typename Base > struct accessor_selector { using type = std::conditional_t< message_mutability_t::immutable_message == Mutability, msg_accessors_t<Base, typename Base::payload_type const>, msg_accessors_t<Base, typename Base::payload_type> >; };
يمكنك فقط الانتباه إلى حساب المعلمة Return_Type. واحدة من تلك الحالات القليلة حيث شرق const مفيد ؛)
حسنًا ، لزيادة إمكانية قراءة التعليمة البرمجية التالية ، خيار أكثر إحكاما للتعامل معها:
template< message_mutability_t Mutability, typename Base > using accessor_selector_t = typename accessor_selector<Mutability, Base>::type;
الخلف النهائي message_holder_t
الآن يمكنك إلقاء نظرة على ماهية message_holder_t
، والتي تتطلب تنفيذ كل هذه الفئات الأساسية والبيانات الوصفية (تتم إزالة جزء من أساليب إنشاء مثيل للرسالة المخزنة في message_holder من التنفيذ):
template< typename Msg, message_ownership_t Ownership = message_ownership_t::autodetected > class message_holder_t : public details::message_holder_details::accessor_selector_t< details::message_mutability_traits<Msg>::mutability, details::message_holder_details::impl_selector_t<Msg, Ownership> > { using base_type = details::message_holder_details::accessor_selector_t< details::message_mutability_traits<Msg>::mutability, details::message_holder_details::impl_selector_t<Msg, Ownership> >; public : using payload_type = typename base_type::payload_type; using envelope_type = typename base_type::envelope_type; using base_type::base_type; friend void swap( message_holder_t & a, message_holder_t & b ) noexcept { using std::swap; swap( a.message_reference(), b.message_reference() ); } };
في الواقع ، كان كل ما قمنا بتحليله أعلاه مطلوبًا من أجل تسجيل هذه "الدعوة" الخاصة بعمليتي تعريف:
details::message_holder_details::accessor_selector_t< details::message_mutability_traits<Msg>::mutability, details::message_holder_details::impl_selector_t<Msg, Ownership> >
لأن ليس هذا هو الخيار الأول ، ولكن نتيجة لتبسيط وتقليل الكود ، يمكنني القول أن الأشكال المدمجة للبيانات الوصفية تقلل إلى حد كبير مقدار الشفرة وتزيد من قابليتها للفهم (إذا كان من المناسب عمومًا التحدث عن الفهم هنا).
وماذا سيحدث لو ...
ولكن إذا كانت لغة C ++ في if constexpr
قوية كما static if
في D ، فيمكنك كتابة شيء مثل:
نسخة افتراضية مع أكثر تقدما إذا constexpr template< typename Msg, message_ownership_t Ownership = message_ownership_t::autodetected > class message_holder_t { static constexpr const message_mutability_t Mutability = details::message_mutability_traits<Msg>::mutability; static constexpr const message_ownership_t Actual_Ownership = (message_ownership_t::unique == Ownership || (message_mutability_t::mutable_msg == Mutability && message_ownership_t::autodetected == Ownership)) ? message_ownership_t::unique : message_ownership_t::shared; public : using payload_type = typename message_payload_type< Msg >::payload_type; using envelope_type = typename message_payload_type< Msg >::envelope_type; private : using getter_return_type = std::conditional_t< message_mutability_t::immutable_msg == Mutability, payload_type const, payload_type >; public : message_holder_t() noexcept = default; message_holder_t( intrusive_ptr_t< envelope_type > mf ) noexcept : m_msg{ std::move(mf) } {} if constexpr(message_ownership_t::unique == Actual_Ownership ) { message_holder_t( const message_holder_t & ) = delete; message_holder_t( message_holder_t && ) noexcept = default; message_holder_t & operator=( const message_holder_t & ) = delete; message_holder_t & operator=( message_holder_t && ) noexcept = default; } friend void swap( message_holder_t & a, message_holder_t & b ) noexcept { using std::swap; swap( a.m_msg, b.m_msg ); } [[nodiscard]] getter_return_type * get() const noexcept { return get_const_ptr( m_msg ); } [[nodiscard]] getter_return_type & operator * () const noexcept { return *get(); } [[nodiscard]] getter_return_type * operator->() const noexcept { return get(); } if constexpr(message_ownership_t::shared == Actual_Ownership) { [[nodiscard]] intrusive_ptr_t< envelope_type > make_reference() const noexcept { return m_msg; } } else { [[nodiscard]] intrusive_ptr_t< envelope_type > make_reference() noexcept { return { std::move(m_msg) }; } } private : intrusive_ptr_t< envelope_type > m_msg; };
, . C++ :(
( C++ "" ).
, , ++. , , , . , message_holder_t
. , , if constexpr
.
استنتاج
, C++. , . , , .
, .
, , ++ , . , . , , . , . C++98/03 , C++11 .