تتناول هذه المقالة أسباب وطرق تجنب السلوك غير المحدد عند الوصول إلى مفردة في c ++ الحديثة. يتم توفير أمثلة التعليمات البرمجية المفرد. لا شيء مترجم محددة ، كل ذلك وفقا للمعيار.
مقدمة
للبدء ، أوصي بأن تقرأ مقالات أخرى عن سينجلتون على حبري:
ثلاثة سنون من نمط Singletonسينجلتون والحالات المشتركة3 طرق لكسر مبدأ المسؤولية الفرديةسينجلتون - نمط أو مضادات؟باستخدام نمط المفردوأخيرًا ، مقال تطرق إلى نفس الموضوع ، لكنه تباطأ (فقط بسبب عدم مراعاة العيوب والقيود):
كائنات tialized (أي ، الكائنات
المنفردة والكائن العمرالتالي:
- هذا ليس مقالًا عن الخصائص المعمارية لـ singleton ؛
- هذا ليس مقالًا "كيف يصنع منفردًا أبيض ورقيقًا من مفردة فظيعة ورهيبة" ؛
- هذه ليست حملة فردية ؛
- انها ليست حملة صليبية ضد المفرد.
- هذا ليس مقال نهاية سعيدة.
هذه المقالة تدور حول جانب واحد مهم للغاية ، ولكن لا يزال الجانب التقني لاستخدام المفرد في C ++ الحديثة. يتم إيلاء الاهتمام الرئيسي في المقال لحظة تدمير المفرد ، كما في معظم المصادر ، يتم الكشف عن قضية التدمير بشكل سيء. عادة ، يتم التركيز على اللحظة التي تم فيها إنشاء المفرد ، وحول التدمير ، في أحسن الأحوال ، فإنه يقول شيئًا مثل "تم تدميره بترتيب عكسي".
سوف أطلب منك اتباع نطاق المقالة في التعليقات ، خاصةً عدم ترتيب نمط المفرد مقابل holivar المفرد.لذلك دعونا نذهب.
ماذا يقول المعيار
ونقلت هي من C + 14 المسودة النهائية N3936 ، كما مسودات C ++ 17 المتاحة غير محددة "نهائية".
أعطي القسم الأكثر أهمية في مجملها. يتم تسليط الضوء على الأماكن الهامة من قبلي.
3.6.3 الإنهاء [basic.start.term]
1. يتم استدعاء المدمرات (12.4) للكائنات التي تمت تهيئتها (أي الكائنات التي بدأ عمرها الافتراضي (3.8)) مع مدة تخزين ثابتة كنتيجة للعودة من الرئيسي ونتيجة لاستدعاء std :: exit (18.5). يتم استدعاء مدمرات الكائنات التي تمت تهيئتها والتي لها مدة تخزين لمؤشر ترابط ضمن مؤشر ترابط معين كنتيجة للعودة من الوظيفة الأولية لمؤشر الترابط ونتيجةً لذلك استدعاء مؤشر الترابط std :: exit. يتم إكمال تسلسل عمليات التدمير لكل الكائنات التي تمت تهيئتها والتي لها مدة تخزين لمؤشر الترابط ضمن هذا الخيط قبل بدء أدوات التدمير لأي كائن ذي مدة تخزين ثابتة. إذا تم إكمال تسلسل المُنشئ أو التهيئة الديناميكية لكائن ذي مدة تخزين مؤشر ترابط قبل الآخر ، فسيتم إكمال تسلسل المدمر الثاني قبل بدء المدمر الأول. إذا تم إكمال تسلسل المُنشئ أو التهيئة الديناميكية لكائن ذي مدة تخزين ثابتة قبل أخرى ، فسيتم إكمال تسلسل المدمرة الثانية قبل بدء المدمر الأول. [ملاحظة: هذا التعريف يسمح بالتدمير المتزامن. –ملاحظة] إذا تم تهيئة كائن بشكل ثابت ، يتم إتلاف الكائن بنفس الترتيب كما لو تم تهيئة الكائن ديناميكيًا. بالنسبة لكائن الصفيف أو نوع الفئة ، يتم إتلاف كافة المشاريع الفرعية لذلك الكائن قبل إتلاف أي كائن نطاق كتلة له مدة تخزين ثابتة يتم تهيئتها أثناء إنشاء المشاريع الفرعية. إذا تم إنهاء تدمير كائن ذي مدة تخزين ثابت أو خيط تخزين مؤقت عبر استثناء ، فسيتم استدعاء std :: terminate (15.5.1).
2. إذا كانت الوظيفة تحتوي على كائن كتلة نطاق ثابت أو مدة تخزين مؤشر الترابط التي تم تدميرها وتسمى الوظيفة أثناء تدمير كائن مع مدة تخزين ثابت أو ثابت ، فإن البرنامج لديه سلوك غير محدد إذا مر تدفق التحكم من خلال تعريف كائن blockscope المدمر مسبقًا. وبالمثل ، فإن السلوك غير معرف إذا تم استخدام كائن كتلة النطاق بشكل غير مباشر (أي من خلال مؤشر) بعد تدميره.
3. في حالة إجراء تسلسل لاستكمال تهيئة كائن ذي مدة تخزين ثابتة قبل استدعاء std :: atexit (انظر "cstdlib" ، 18.5) ، يتم ترتيب استدعاء الوظيفة التي تم تمريرها إلى std :: atexit قبل المكالمة إلى المدمر للكائن. إذا تم إجراء تسلسل استدعاء std :: atexit قبل إتمام تهيئة كائن ذي مدة تخزين ثابتة ، فإن التسلسل إلى destructor للكائن يتم تسلسله قبل أن يتم تمرير الاستدعاء للدالة إلى std :: atexit. إذا تم إجراء تسلسل لاستدعاء std :: atexit قبل إجراء مكالمة أخرى إلى std :: atexit ، يتم إجراء استدعاء للوظيفة التي تم تمريرها إلى المكالمة std :: atexit الثانية قبل تسلسل استدعاء الوظيفة إلى المكالمة std :: atexit الأولى .
4. إذا كان هناك استخدام لكائن أو وظيفة مكتبة قياسية غير مسموح بها داخل معالجات الإشارة (18.10) لا يحدث قبل (1.10) الانتهاء من تدمير الكائنات ذات مدة التخزين الثابتة وتنفيذ الوظائف المسجلة std :: atexit (18.5) ) ، البرنامج لديه سلوك غير محدد. [ملاحظة: إذا كان هناك استخدام لكائن ذي مدة تخزين ثابتة لا يحدث قبل تدمير الكائن ، فإن البرنامج لديه سلوك غير محدد. إنهاء كل مؤشر ترابط قبل استدعاء std :: exit أو الخروج من main يكفي ، ولكن ليس من الضروري ، لتلبية هذه المتطلبات. تسمح هذه المتطلبات لمدراء سلاسل العمليات ككائنات مدة تخزين ثابتة. -ملاحظة]
5. استدعاء دالة std :: abort () المعلنة في "cstdlib" ينهي البرنامج دون تنفيذ أي مدمرات ودون استدعاء الوظائف التي تم تمريرها إلى std :: atexit () أو std :: at_quick_exit ().
تفسير:
- يتم تنفيذ تدمير الكائنات ذات مدة تخزين الخيوط بالترتيب العكسي لإنشائها ؛
- بعد ذلك بدقة ، يتم تدمير الكائنات ذات مدة التخزين الثابتة ويتم إجراء المكالمات إلى الوظائف المسجلة مع std :: atexit بالترتيب العكسي لإنشاء مثل هذه الكائنات وتسجيل هذه الوظائف ؛
- تحتوي محاولة الوصول إلى كائن معطوب مع مدة تخزين مؤشر ترابط أو مدة تخزين ثابت على سلوك غير محدد. لم يتم توفير إعادة تهيئة هذه الكائنات.
ملاحظة: يشار إلى المتغيرات العامة في المعيار باسم "المتغير غير المحلي مع مدة تخزين ثابتة". نتيجةً لذلك ، اتضح أن جميع المتغيرات العامة وجميع الأحاديات (الإحصائيات المحلية) وجميع المكالمات إلى std :: atexit تقع في قائمة انتظار LIFO واحدة عند إنشائها / تسجيلها.
وترد المعلومات المفيدة لهذه المادة أيضًا في القسم
3.6.2 تهيئة المتغيرات غير المحلية [basic.start.init] . أحمل فقط أهم:
التهيئة الديناميكية لمتغير غير محلي مع مدة تخزين ثابتة إما مرتبة أو غير مرتبة. [...] يجب تهيئة المتغيرات ذات التهيئة المطلوبة المعرفة ضمن وحدة ترجمة واحدة بترتيب تعريفاتها في وحدة الترجمة.
التفسير (مع مراعاة النص الكامل للقسم): تتم تهيئة المتغيرات العامة داخل وحدة ترجمة واحدة بترتيب الإعلان.
ماذا سيكون في الكود
يتم نشر جميع أمثلة التعليمات البرمجية الواردة في المقالة على
جيثب .
يتكون الكود من ثلاث طبقات ، كما لو كان مكتوبًا بواسطة أشخاص مختلفين:
- المفرد.
- فائدة (فئة باستخدام المفردة) ؛
- المستخدم (المتغيرات العالمية والرئيسية).
يشبه Singleton والأداة المساعدة مكتبة تابعة لجهة خارجية ، والمستخدم هو المستخدم.
طبقة الأداة المساعدة مصممة لعزل طبقة المستخدم عن طبقة مفردة. في الأمثلة ، لدى المستخدم الفرصة للوصول إلى المفرد ، لكننا سنتصرف كما لو كان ذلك مستحيلًا.
يقوم المستخدم أولاً بعمل كل شيء بشكل صحيح ، وبعد ذلك ينفض كل شيء بنقرة من الرسغ. أولاً نحاول إصلاحه في طبقة الأداة المساعدة ، وإذا لم ينجح الأمر ، فحينئذٍ في الطبقة المفردة.
في الكود ، سوف نسير باستمرار على طول الحافة - الآن على الجانب الخفيف ، ثم في الظلام. لتسهيل التبديل إلى الجانب المظلم ، تم اختيار الحالة الأكثر صعوبة - الوصول إلى مفردة من أداة إتلاف الأدوات المساعدة.
لماذا هي حالة الاتصال من المدمر أصعب؟ نظرًا لأنه يمكن استدعاء أداة إتلاف الأدوات المساعدة في عملية تقليل التطبيق إلى الحد الأدنى ، عندما يصبح السؤال "هل تم إتلاف المفردة أم لا" يصبح ذا صلة.
القضية هي نوع من الاصطناعية. في الممارسة العملية ، ليست هناك حاجة لدعوات من المفرد من المدمر. حتى عند الحاجة. على سبيل المثال ، لتسجيل تدمير الكائنات.
وتستخدم ثلاث فئات من المفردة:
- SingletonClassic - لا مؤشرات ذكية. في الواقع ، إنه ليس كلاسيكيًا تمامًا بشكل مباشر ، ولكنه بالتأكيد الأكثر كلاسيكية بين الثلاثة الذين تم بحثهم ؛
- SingletonShared - مع الأمراض المنقولة جنسيا :: shared_ptr؛
- SingletonWeak - مع الأمراض المنقولة جنسيا :: ضعيف.
جميع المفردات هي قوالب. يتم استخدام المعلمة قالب ليرث منه. في معظم الأمثلة ، يتم تحديدها بواسطة فئة Payload ، والتي توفر وظيفة عامة واحدة لإضافة البيانات إلى std :: set.
يحاول أداة إتلاف الأداة المساعدة في معظم الأمثلة ملء مئات القيم هناك. يتم استخدام الإخراج التشخيصي إلى وحدة التحكم أيضًا من مُنشئ مفرد ، ومدمّر مفرد ، ومثيل ().
لماذا من الصعب جدا؟ لتسهيل فهم أننا في الجانب المظلم. مناشدة المفرد المدمر هو سلوك غير محدد ، ولكن قد لا يتجلى بأي شكل من الأشكال خارجياً. من المؤكد أن حشو القيم في std :: set المدمر لا يضمن بالتأكيد المظاهر الخارجية ، ولكن لا توجد طريقة أكثر موثوقية (في الواقع ، في دول مجلس التعاون الخليجي في نظام Linux في أمثلة غير صحيحة مع المفرد الكلاسيكي ، تم بنجاح STD :: set المدمرة ، وفي MSVS ضمن ويندوز - توقف). مع سلوك غير محدد ، قد
لا يحدث الإخراج إلى وحدة التحكم. لذلك في الأمثلة الصحيحة ، نتوقع عدم الوصول إلى المثيل () بعد المدمر ، وكذلك عدم وجود تعطل وغياب تعليق ، وفي الحالات غير الصحيحة ، إما وجود مثل هذا النداء أو التعطل أو تعليق أو كل ذلك مرة واحدة في أي مجموعات أو أيًا كان.
المفردة الكلاسيكية
Payload.h#pragma once #include <set> class Payload { public: Payload() = default; ~Payload() = default; Payload(const Payload &) = delete; Payload(Payload &&) = delete; Payload& operator=(const Payload &) = delete; Payload& operator=(Payload &&) = delete; void add(int value) { m_data.emplace(value); } private: std::set<int> m_data; };
SingletonClassic.h #pragma once #include <iostream> template<typename T> class SingletonClassic : public T { public: ~SingletonClassic() { std::cout << "~SingletonClassic()" << std::endl; } SingletonClassic(const SingletonClassic &) = delete; SingletonClassic(SingletonClassic &&) = delete; SingletonClassic& operator=(const SingletonClassic &) = delete; SingletonClassic& operator=(SingletonClassic &&) = delete; static SingletonClassic& instance() { std::cout << "instance()" << std::endl; static SingletonClassic inst; return inst; } private: SingletonClassic() { std::cout << "SingletonClassic()" << std::endl; } };
مثال على SingletonClassic 1
Classic_Example1_correct.cpp #include "SingletonClassic.h" #include "Payload.h" #include <memory> class ClassicSingleThreadedUtility { public: ClassicSingleThreadedUtility() { // To ensure that singleton will be constucted before utility SingletonClassic<Payload>::instance(); } ~ClassicSingleThreadedUtility() { auto &instance = SingletonClassic<Payload>::instance(); for ( int i = 0; i < 100; ++i ) instance.add(i); } }; // 1. Create an empty unique_ptr // 2. Create singleton (because of modified ClassicSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<ClassicSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<ClassicSingleThreadedUtility>(); // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order is correct int main() { return 0; }
إخراج وحدة التحكممثيل ()
سينجلتون كلاسيك ()
مثيل ()
~ SingletonClassic ()
تقوم الأداة المساعدة باستدعاء المفرد في المنشئ للتأكد من إنشاء المفرد قبل إنشاء الأداة المساعدة.
ينشئ المستخدم std :: unique_ptr: واحد فارغ ، والثاني يحتوي على الأداة المساعدة.
ترتيب الخلق:
- الأمراض المنقولة جنسيا فارغة :: unique_ptr.
- المفردة ؛
- فائدة.
وبناء على ذلك ، ترتيب التدمير:
- فائدة ؛
- المفردة ؛
- الأمراض المنقولة جنسيا فارغة :: unique_ptr.
استدعاء من الأداة المساعدة destructor إلى المفرد هو الصحيح.
مثال على SingletonClassic 2
كل شيء هو نفسه ، ولكن المستخدم استغرق الأمر ودمر كل شيء بسطر واحد.
Classic_Example2_incorrect.cpp #include "SingletonClassic.h" #include "Payload.h" #include <memory> class ClassicSingleThreadedUtility { public: ClassicSingleThreadedUtility() { // To ensure that singleton will be constucted before utility SingletonClassic<Payload>::instance(); } ~ClassicSingleThreadedUtility() { auto &instance = SingletonClassic<Payload>::instance(); for ( int i = 0; i < 100; ++i ) instance.add(i); } }; // 1. Create an empty unique_ptr // 2. Create singleton (because of modified ClassicSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<ClassicSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<ClassicSingleThreadedUtility>(); // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order seems to be correct ... int main() { // ... but user swaps unique_ptrs emptyUnique.swap(utilityUnique); // Guaranteed destruction order is still the same: // - utilityUnique; // - singleton; // - emptyUnique, // but now utilityUnique is empty, and emptyUnique is filled, // so destruction order is incorrect return 0; }
إخراج وحدة التحكممثيل ()
سينجلتون كلاسيك ()
~ SingletonClassic ()
مثيل ()
يتم الحفاظ على ترتيب الخلق والدمار. يبدو أن كل شيء لا يزال. لكن لا. عن طريق استدعاء blankUnique.swap (utilityUnique) ، ارتكب المستخدم سلوكًا غير محدد.
لماذا يفعل المستخدم مثل هذه الأشياء الغبية؟ لأنه لا يعرف شيئًا عن البنية الداخلية للمكتبة ، والتي وفرت له منفردًا وفائدة.
وإذا كنت تعرف الهيكل الداخلي للمكتبة؟ ... ثم على أي حال ، من السهل للغاية المشاركة في الكود الحقيقي. وعليك الخروج عن طريق debag المؤلمة ، لأن لفهم ما حدث بالضبط لن يكون سهلا.
لماذا لا تتطلب المكتبة لاستخدامها بشكل صحيح؟ حسنًا ، هناك كل أنواع الإرساء التي يجب أن تكتبها وأمثلة ... ولماذا لا تجعل مكتبة ليس من السهل أن تفسد؟
مثال على SingletonClassic 3
في سياق إعداد المقالة لعدة أيام ، اعتقدت أنه من المستحيل القضاء على السلوك غير المحدد من المثال السابق في طبقة الأداة المساعدة ، ولم يكن الحل متاحًا إلا في الطبقة المفردة. لكن مع مرور الوقت ، تم التوصل إلى حل.
قبل فتح المفسدين بالكود والشرح ، أقترح على القارئ محاولة إيجاد طريقة للخروج من الموقف بمفرده (فقط في طبقة الأداة المساعدة!). أنا لا أستبعد أن هناك حلول أفضل.
Classic_Example3_correct.cpp #include "SingletonClassic.h" #include "Payload.h" #include <memory> #include <iostream> class ClassicSingleThreadedUtility { public: ClassicSingleThreadedUtility() { thread_local auto flag_strong = std::make_shared<char>(0); m_flag_weak = flag_strong; SingletonClassic<Payload>::instance(); } ~ClassicSingleThreadedUtility() { if ( !m_flag_weak.expired() ) { auto &instance = SingletonClassic<Payload>::instance(); for ( int i = 0; i < 100; ++i ) instance.add(i); } } private: std::weak_ptr<char> m_flag_weak; }; // 1. Create an empty unique_ptr // 2. Create singleton (because of modified ClassicSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<ClassicSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<ClassicSingleThreadedUtility>(); // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order seems to be correct ... int main() { // ... but user swaps unique_ptrs emptyUnique.swap(utilityUnique); { // To demonstrate normal processing before application ends auto utility = ClassicSingleThreadedUtility(); } // Guaranteed destruction order is still the same: // - utilityUnique; // - singleton; // - emptyUnique, // but now utilityUnique is empty, and emptyUnique is filled, // so destruction order is incorrect ... // ... but utility uses a variable with thread storage duration to detect thread termination. return 0; }
إخراج وحدة التحكممثيل ()
سينجلتون كلاسيك ()
مثيل ()
مثيل ()
~ SingletonClassic ()
توضيحالمشكلة تحدث فقط عند تصغير التطبيق. يمكن التخلص من السلوك غير المحدد عن طريق تدريس الأداة المساعدة للتعرف على الحد الأدنى للتطبيق. للقيام بذلك ، استخدمنا متغيرًا قويًا للإعلام من النوع std :: shared_ptr ، يحتوي على مؤهل مدة تخزين مؤشر ترابط (راجع مقتطفات من المعيار في المقالة أعلاه) - إنه يشبه ثابتًا ، لكنه يتم إتلافه فقط عندما ينتهي مؤشر الترابط الحالي قبل إتلاف أي إحصائيات ، بما في ذلك قبل التدمير المفرد. المتغير flag_strong هو أحد الدفق بالكامل ، ويقوم كل مثيل للأداة المساعدة بتخزين نسخته الضعيفة.
بمعنى ضيق ، يمكن أن يسمى الحل الاختراق ، لأنه إنه غير مباشر وغير واضح. بالإضافة إلى ذلك ، يحذر مبكرًا جدًا ، وأحيانًا (في تطبيق متعدد الخيوط) يحذر بشكل عام من الخطأ. ولكن بمعنى واسع ، هذا ليس اختراقًا ، ولكنه حل يتم تعريفه بالكامل بواسطة الخصائص القياسية - كل من العيوب والمزايا.
SingletonShared
دعنا ننتقل إلى مفردة معدلة بناءً على الأمراض المنقولة جنسياً :: shared_ptr.
SingletonShared.h #pragma once #include <memory> #include <iostream> template<typename T> class SingletonShared : public T { public: ~SingletonShared() { std::cout << "~SingletonShared()" << std::endl; } SingletonShared(const SingletonShared &) = delete; SingletonShared(SingletonShared &&) = delete; SingletonShared& operator=(const SingletonShared &) = delete; SingletonShared& operator=(SingletonShared &&) = delete; static std::shared_ptr<SingletonShared> instance() { std::cout << "instance()" << std::endl; // "new" and no std::make_shared because of private c-tor static auto inst = std::shared_ptr<SingletonShared>(new SingletonShared); return inst; } private: SingletonShared() { std::cout << "SingletonShared()" << std::endl; } };
Ai-ai-ai ، يجب عدم استخدام المشغل الجديد في الكود الحديث ، بدلاً من ذلك ، هناك حاجة إلى std :: make_shared! وهذا ما يمنعه منشئ المفردة الخاصة.
ها! لدي مشكلة أيضا! أعلن الأمراض المنقولة جنسيا :: make_shared freind فردية! ... والحصول على تباين لل antipattern PublicMorozov: باستخدام نفس الأمراض المنقولة جنسيا :: make_shared ، سيكون من الممكن إنشاء حالات إضافية من المفرد التي لم يتم توفيرها من قبل الهندسة المعمارية.
SingletonShared أمثلة 1 و 2
تتوافق تماما مع الأمثلة رقم 1 و 2 للنسخة الكلاسيكية. تم إجراء تغييرات كبيرة على طبقة المفرد فقط ، ظلت الأداة المساعدة كما هي. كما هو الحال في الأمثلة مع المفرد الكلاسيكي ، المثال -1 صحيح ، والمثال 2 يوضح السلوك غير المحدد.
Shared_Example1_correct.cpp #include "SingletonShared.h" #include <Payload.h> #include <memory> class SharedSingleThreadedUtility { public: SharedSingleThreadedUtility() { // To ensure that singleton will be constucted before utility SingletonShared<Payload>::instance(); } ~SharedSingleThreadedUtility() { if ( auto instance = SingletonShared<Payload>::instance() ) for ( int i = 0; i < 100; ++i ) instance->add(i); } }; // 1. Create an empty unique_ptr // 2. Create singleton (because of modified SharedSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<SharedSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<SharedSingleThreadedUtility>(); // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order is correct int main() { return 0; }
إخراج وحدة التحكممثيل ()
SingletonShared ()
مثيل ()
~ SingletonShared ()
Shared_Example2_incorrect.cpp #include "SingletonShared.h" #include "Payload.h" #include <memory> class SharedSingleThreadedUtility { public: SharedSingleThreadedUtility() { // To ensure that singleton will be constucted before utility SingletonShared<Payload>::instance(); } ~SharedSingleThreadedUtility() { // Sometimes this check may result as "false" even for destroyed singleton // preventing from visual effects of undefined behaviour ... //if ( auto instance = SingletonShared::instance() ) // for ( int i = 0; i < 100; ++i ) // instance->add(i); // ... so this code will demonstrate UB in colour auto instance = SingletonShared<Payload>::instance(); for ( int i = 0; i < 100; ++i ) instance->add(i); } }; // 1. Create an empty unique_ptr // 2. Create singleton (because of modified SharedSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<SharedSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<SharedSingleThreadedUtility>(); // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order seems to be correct ... int main() { // ... but user swaps unique_ptrs emptyUnique.swap(utilityUnique); // Guaranteed destruction order is the same: // - utilityUnique; // - singleton; // - emptyUnique, // but now utilityUnique is empty, and emptyUnique is filled, // so destruction order is incorrect return 0; }
إخراج وحدة التحكممثيل ()
SingletonShared ()
~ SingletonShared ()
مثيل ()
مثال على SingletonShared 3
والآن سنحاول حل هذه المشكلة بشكل أفضل من المثال رقم 3 من الكلاسيكيات.
الحل واضح: تحتاج فقط إلى إطالة عمر المفرد من خلال تخزين نسخة من std :: shared_ptr التي تم إرجاعها بواسطة المفرد في الأداة المساعدة. وهذا الحل ، مع اكتمال SingletonShared ، تم استنساخه على نطاق واسع في المصادر المفتوحة.
Shared_Example3_correct.cpp #include "SingletonShared.h" #include "Payload.h" #include <memory> class SharedSingleThreadedUtility { public: SharedSingleThreadedUtility() // To ensure that singleton will be constucted before utility : m_singleton(SingletonShared<Payload>::instance()) { } ~SharedSingleThreadedUtility() { // Sometimes this check may result as "false" even for destroyed singleton // preventing from visual effects of undefined behaviour ... //if ( m_singleton ) // for ( int i = 0; i < 100; ++i ) // m_singleton->add(i); // ... so this code will allow to demonstrate UB in colour for ( int i = 0; i < 100; ++i ) m_singleton->add(i); } private: // A copy of smart pointer, not a reference std::shared_ptr<SingletonShared<Payload>> m_singleton; }; // 1. Create an empty unique_ptr // 2. Create singleton (because of SharedSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<SharedSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<SharedSingleThreadedUtility>(); int main() { // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order is correct ... // ... but user swaps unique_ptrs emptyUnique.swap(utilityUnique); // Guaranteed destruction order is the same: // - utilityUnique; // - singleton; // - emptyUnique, // but now utilityUnique is empty, and emptyUnique is filled, // so destruction order is incorrect... // ... but utility have made a copy of shared_ptr when it was available, // so it's correct again. return 0; }
إخراج وحدة التحكممثيل ()
SingletonShared ()
~ SingletonShared ()
والآن ، الاهتمام ، والسؤال هو:
هل تريد حقا إطالة عمر مفردة؟أو هل تريد التخلص من السلوك غير المحدد ، واختيار تمديد الحياة كوسيلة ملقاة على السطح؟
الخلل النظري في شكل استبدال الأهداف عن طريق يؤدي إلى خطر الجمود (أو مرجع دوري - نسميها ما تريد).
نعم nuuuuu ، هذه هي الطريقة التي يجب أن تحاول بها بجد! يجب عليك الخروج بمثل هذا الوقت الطويل ، وبالتأكيد لن تفعل ذلك عن طريق الصدفة!CallbackPayload.h #pragma once #include <functional> class CallbackPayload { public: CallbackPayload() = default; ~CallbackPayload() = default; CallbackPayload(const CallbackPayload &) = delete; CallbackPayload(CallbackPayload &&) = delete; CallbackPayload& operator=(const CallbackPayload &) = delete; CallbackPayload& operator=(CallbackPayload &&) = delete; void setCallback(std::function<void()> &&fn) { m_callbackFn = std::move(fn); } private: std::function<void()> m_callbackFn; };
SomethingWithVeryImportantDestructor.h #pragma once #include <iostream> class SomethingWithVeryImportantDestructor { public: SomethingWithVeryImportantDestructor() { std::cout << "SomethingWithVeryImportantDestructor()" << std::endl; } ~SomethingWithVeryImportantDestructor() { std::cout << "~SomethingWithVeryImportantDestructor()" << std::endl; } SomethingWithVeryImportantDestructor(const SomethingWithVeryImportantDestructor &) = delete; SomethingWithVeryImportantDestructor(SomethingWithVeryImportantDestructor &&) = delete; SomethingWithVeryImportantDestructor& operator=(const SomethingWithVeryImportantDestructor &) = delete; SomethingWithVeryImportantDestructor& operator=(SomethingWithVeryImportantDestructor &&) = delete; };
Shared_Example4_incorrect.cpp #include "SingletonShared.h" #include "CallbackPayload.h" #include "SomethingWithVeryImportantDestructor.h" class SharedSingleThreadedUtility { public: SharedSingleThreadedUtility()
إخراج وحدة التحكممثيل ()
SingletonShared ()
SharedSingleThreadedUtility ()
SomethingWithVeryImportantDestructor ()
تم إنشاء مفردة.
تم إنشاء أداة مساعدة.
تم إنشاء شيء مدمر
مهم جدًا (لقد قمت بإضافة هذا للتخويف ، لأنه توجد على الإنترنت منشورات مثل "حسنًا ، لن يتم استدعاء المدمر المفرد ، لذا ما هذا ، يجب أن يكون موجودًا طوال الوقت البرامج ").
ولكن لم يتم استدعاء أي مدمر لأي من هذه الأشياء!
بسبب ماذا؟ بسبب استبدال الأهداف عن طريق.
SingletonWeak
SingletonWeak.h #pragma once #include <memory> #include <iostream> template<typename T> class SingletonWeak : public T { public: ~SingletonWeak() { std::cout << "~SingletonWeak()" << std::endl; } SingletonWeak(const SingletonWeak &) = delete; SingletonWeak(SingletonWeak &&) = delete; SingletonWeak& operator=(const SingletonWeak &) = delete; SingletonWeak& operator=(SingletonWeak &&) = delete; static std::weak_ptr<SingletonWeak> instance() { std::cout << "instance()" << std::endl; // "new" and no std::make_shared because of private c-tor static auto inst = std::shared_ptr<SingletonWeak>(new SingletonWeak); return inst; } private: SingletonWeak() { std::cout << "SingletonWeak()" << std::endl; } };
مثل هذا التعديل للفرد المفرد في المصادر المفتوحة ، إذا أعطى ، ليس بالتأكيد في كثير من الأحيان. لقد قابلت بعض المتغيرات الغريبة التي تحولت من الداخل إلى الخارج باستخدام std ::ضعف_ptr ، والذي يبدو أنه مستخدم ، والذي ، على ما يبدو ، لا يقدم فائدة أكثر من إطالة عمر المفرد:
الخيار الذي أقترحه ، عند تطبيقه بشكل صحيح في طبقات المفرد والأدوات المساعدة:
- يحمي من الإجراءات في طبقة المستخدم الموضحة في الأمثلة أعلاه ، بما في ذلك يمنع حالة توقف تام ؛
- يحدد وقت طي التطبيق بشكل أكثر دقة من تطبيق thread_local في Classic_Example3_correct ، أي يتيح لك الاقتراب من الحافة ؛
- لا أعاني من المشكلة النظرية المتمثلة في استبدال الأهداف بالوسائل (لا أعرف ما إذا كان أي شيء ملموس آخر غير الجمود يمكن أن يظهر من هذه المشكلة النظرية).
ومع ذلك ، هناك عيب: إطالة عمر مفردة لا
يزال يمكن أن تسمح
لها أن تقترب أكثر من الحافة.
مثال على SingletonWeak 1
مشابه Shared_Example3_correct.cpp.
Weak_Example1_correct.cpp #include "SingletonWeak.h" #include "Payload.h" #include <memory> class WeakSingleThreadedUtility { public: WeakSingleThreadedUtility() // To ensure that singleton will be constucted before utility : m_weak(SingletonWeak<Payload>::instance()) { } ~WeakSingleThreadedUtility() { // Sometimes this check may result as "false" even in case of incorrect usage, // and there's no way to guarantee a demonstration of undefined behaviour in colour if ( auto strong = m_weak.lock() ) for ( int i = 0; i < 100; ++i ) strong->add(i); } private: // A weak copy of smart pointer, not a reference std::weak_ptr<SingletonWeak<Payload>> m_weak; }; // 1. Create an empty unique_ptr // 2. Create singleton (because of WeakSingleThreadedUtility c-tor) // 3. Create utility std::unique_ptr<WeakSingleThreadedUtility> emptyUnique; auto utilityUnique = std::make_unique<WeakSingleThreadedUtility>(); int main() { // This guarantee destruction in order: // - utilityUnique; // - singleton; // - emptyUnique. // This order is correct ... // ... but user swaps unique_ptrs emptyUnique.swap(utilityUnique); // Guaranteed destruction order is the same: // - utilityUnique; // - singleton; // - emptyUnique, // but now utilityUnique is empty, and emptyUnique is filled, // so destruction order is incorrect... // ... but utility have made a weak copy of shared_ptr when it was available, // so it's correct again. return 0; }
إخراج وحدة التحكممثيل ()
SingletonWeak ()
~ SingletonWeak ()
لماذا نحتاج إلى SingletonWeak ، لأن لا أحد يزعج الأداة المساعدة لاستخدام SingletonShared كـ SingletonWeak؟ نعم ، لا أحد يزعج. وحتى لا أحد يزعج الأداة المساعدة لاستخدام SingletonWeak كـ SingletonShared. لكن استخدامها للغرض المقصود هو أسهل قليلاً من استخدامها لأغراض أخرى.
مثال على SingletonWeak 2
تشبه Shared_Example4_incorrect ، لكن حالة توقف تام فقط لا تحدث في هذه الحالة.
Weak_Example2_correct.cpp #include "SingletonWeak.h" #include "CallbackPayload.h" #include "SomethingWithVeryImportantDestructor.h" class WeakSingleThreadedUtility { public: WeakSingleThreadedUtility()
إخراج وحدة التحكممثيل ()
SingletonWeak ()
ضعيف وحيدالموضوع ()
SomethingWithVeryImportantDestructor ()
~ SingletonWeak ()
~ SomethingWithVeryImportantDestructor ()
~ ضعف ضعيفالمستخدمين ()
بدلا من الاستنتاج
وماذا ، مثل هذا التعديل لفرد سينهي السلوك غير المحدد؟ وعدت أنه لن يكون هناك نهاية سعيدة. توضح الأمثلة التالية أن إجراءات التخريب الماهرة في طبقة المستخدم يمكن أن تدمر حتى مكتبة التفكير الصحيحة باستخدام مفردة (لكن يجب أن نعترف بأنه لا يمكن القيام بذلك عن طريق الصدفة).
Shared_Example5_incorrect.cpp #include "SingletonShared.h" #include "Payload.h" #include <memory> #include <cstdlib> class SharedSingleThreadedUtility { public: SharedSingleThreadedUtility() // To ensure that singleton will be constucted before utility : m_singleton(SingletonShared<Payload>::instance()) { } ~SharedSingleThreadedUtility() { // Sometimes this check may result as "false" even for destroyed singleton // preventing from visual effects of undefined behaviour ... //if ( m_singleton ) // for ( int i = 0; i < 100; ++i ) // m_singleton->add(i); // ... so this code will allow to demonstrate UB in colour for ( int i = 0; i < 100; ++i ) m_singleton->add(i); } private: // A copy of smart pointer, not a reference std::shared_ptr<SingletonShared<Payload>> m_singleton; }; void cracker() { SharedSingleThreadedUtility(); } // 1. Register cracker() using std::atexit // 2. Create singleton // 3. Create utility auto reg = [](){ std::atexit(&cracker); return 0; }(); auto utility = SharedSingleThreadedUtility(); // This guarantee destruction in order: // - utility; // - singleton. // This order is correct. // Additionally, there's a copy of shared_ptr in the class instance... // ... but there was std::atexit registered before singleton, // so cracker() will be invoked after destruction of utility and singleton. // There's second try to create a singleton - and it's incorrect. int main() { return 0; }
إخراج وحدة التحكممثيل ()
SingletonShared ()
~ SingletonShared ()
مثيل ()
Weak_Example3_incorrect.cpp #include "SingletonWeak.h" #include "Payload.h" #include <memory> #include <cstdlib> class WeakSingleThreadedUtility { public: WeakSingleThreadedUtility() // To ensure that singleton will be constucted before utility : m_weak(SingletonWeak<Payload>::instance()) { } ~WeakSingleThreadedUtility() { // Sometimes this check may result as "false" even in case of incorrect usage, // and there's no way to guarantee a demonstration of undefined behaviour in colour if ( auto strong = m_weak.lock() ) for ( int i = 0; i < 100; ++i ) strong->add(i); } private: // A weak copy of smart pointer, not a reference std::weak_ptr<SingletonWeak<Payload>> m_weak; }; void cracker() { WeakSingleThreadedUtility(); } // 1. Register cracker() using std::atexit // 2. Create singleton // 3. Create utility auto reg = [](){ std::atexit(&cracker); return 0; }(); auto utility = WeakSingleThreadedUtility(); // This guarantee destruction in order: // - utility; // - singleton. // This order is correct. // Additionally, there's a copy of shared_ptr in the class instance... // ... but there was std::atexit registered before singleton, // so cracker() will be invoked after destruction of utility and singleton. // There's second try to create a singleton - and it's incorrect. int main() { return 0; }
إخراج وحدة التحكممثيل ()
SingletonWeak ()
~ SingletonWeak ()
مثيل ()