एक सिंगलटन तक पहुँचने पर अपरिभाषित व्यवहार से बचने की तकनीक

आधुनिक सी ++ में एक सिंगलटन तक पहुंचने पर लेख अपरिभाषित व्यवहार से बचने के कारणों और तरीकों पर चर्चा करता है। एकल-थ्रेडेड कोड के उदाहरण दिए गए हैं। संकलक-विशिष्ट कुछ भी नहीं, सभी मानक के अनुसार।

परिचय


शुरू करने के लिए, मेरा सुझाव है कि आप हैबर पर सिंगलटन के बारे में अन्य लेख पढ़ें:

सिंगलटन पैटर्न के तीन युग
सिंगलटन और कॉमन इंस्टैंस
एकल जिम्मेदारी सिद्धांत को तोड़ने के 3 तरीके
सिंगलटन - पैटर्न या एंटीपैटर्न?
सिंगलटन पैटर्न का उपयोग करना

और, अंत में, एक लेख जो एक ही विषय पर छुआ, लेकिन इसमें फिसल गया (यदि केवल इसलिए कि नुकसान और सीमाओं पर विचार नहीं किया गया था):
tialized ऑब्जेक्ट्स (यानी ऑब्जेक्ट्स)
सिंगलटन और ऑब्जेक्ट लाइफटाइम

अगला:

  • यह सिंगलटन के वास्तु गुणों पर एक लेख नहीं है ;
  • यह एक लेख "कैसे एक भयानक और भयानक सिंगलटन से बाहर एक सफेद और शराबी सिंगलटन बनाने के लिए नहीं है ";
  • यह एक एकल अभियान नहीं है ;
  • यह सिंगलटन के खिलाफ धर्मयुद्ध नहीं है;
  • यह एक सुखद अंत लेख नहीं है।

यह लेख आधुनिक C ++ में सिंगलटन का उपयोग करने के एक बहुत महत्वपूर्ण, लेकिन अभी भी तकनीकी पहलू के बारे में है। लेख में मुख्य ध्यान सिंगलटन के विनाश के क्षण पर दिया जाता है, जैसे अधिकांश स्रोतों में, विनाश के मुद्दे का खुलासा किया गया है। आमतौर पर, उस क्षण पर जोर दिया जाता है, जब सिंगलटन बनाया गया था, और विनाश के बारे में, सबसे अच्छे रूप में, यह कुछ ऐसा कहता है जैसे "रिवर्स ऑर्डर में नष्ट हो गया।"

मैं आपको टिप्पणियों में लेख के दायरे का पालन करने के लिए कहूंगा, विशेष रूप से सिंगलटन पैटर्न बनाम सिंगलटन एंटीपैटर्न होलकर की व्यवस्था करने के लिए नहीं।

तो चलिए चलते हैं।

मानक क्या कहता है


उद्धरण C ++ 14 अंतिम ड्राफ्ट N3936 के रूप में हैं उपलब्ध C ++ 17 ड्राफ्ट "अंतिम" के रूप में चिह्नित नहीं हैं।
मैं इसकी संपूर्णता में सबसे महत्वपूर्ण खंड देता हूं। मेरे द्वारा महत्वपूर्ण स्थानों को उजागर किया गया है।

3.6.3 समाप्ति [basic.start.term]

1. प्रारंभिक वस्तुओं के लिए डिस्ट्रक्टर्स (12.4) (यानी, ऐसी वस्तुएं जिनका जीवनकाल (3.8) शुरू हो चुका है) स्थिर भंडारण अवधि के साथ मुख्य से लौटने और एसटीडी कॉलिंग के परिणाम के रूप में कहा जाता है :: बाहर निकलें (18.5)। किसी दिए गए धागे के भीतर थ्रेड स्टोरेज अवधि के साथ प्रारंभिक वस्तुओं के लिए विनाशकों को उस थ्रेड के प्रारंभिक फ़ंक्शन से लौटने और उस थ्रेड कॉलिंग std :: बाहर निकलने के परिणामस्वरूप कहा जाता है। उस थ्रेड के भीतर थ्रेड स्टोरेज अवधि के साथ सभी आरंभीकृत ऑब्जेक्ट्स के लिए डिस्ट्रक्टर्स की पूर्णता को स्थिर स्टोरेज अवधि के साथ किसी भी ऑब्जेक्ट के डिस्ट्रक्टर्स की दीक्षा से पहले अनुक्रमित किया जाता है। यदि थ्रेड स्टोरेज की अवधि के साथ किसी ऑब्जेक्ट का कंस्ट्रक्टर या डायनेमिक इनिशियलाइज़ेशन पूरा होने से पहले, दूसरे के डिस्ट्रक्टर को पूरा करने के लिए पहले के डिस्ट्रॉयर के आरंभ से पहले अनुक्रम किया जाता है। यदि स्थिर भंडारण अवधि के साथ किसी ऑब्जेक्ट का निर्माण या गतिशील प्रारंभिक पूरा होने से पहले दूसरे का अनुक्रम होता है, तो दूसरे के विध्वंसक के पूरा होने से पहले के विनाशकर्ता के आरंभ से पहले अनुक्रम किया जाता है। [नोट: यह परिभाषा समवर्ती विनाश की अनुमति देती है। –और ध्यान दें] यदि किसी वस्तु को वैधानिक रूप से आरम्भ किया जाता है, तो वस्तु उसी क्रम में नष्ट हो जाती है जैसे कि वस्तु को गतिशील रूप से आरम्भ किया गया था। किसी ऑब्जेक्ट या श्रेणी प्रकार के लिए, सब -जेक्ट के निर्माण के दौरान शुरू की गई स्टैटिक स्टोरेज अवधि के साथ किसी भी ब्लॉक-स्कोप ऑब्जेक्ट से पहले उस ऑब्जेक्ट के सभी सबोबिज को नष्ट कर दिया जाता है। यदि स्थिर या थ्रेड स्टोरेज अवधि वाली किसी वस्तु का विनाश एक अपवाद के माध्यम से बाहर निकलता है, तो std :: समाप्ति को कहा जाता है (15.5.1)।

2. यदि किसी फ़ंक्शन में स्थैतिक या थ्रेड स्टोरेज अवधि का ब्लॉक-स्कोप ऑब्जेक्ट है जो नष्ट हो गया है और स्थिर या थ्रेड स्टोरेज अवधि के साथ ऑब्जेक्ट को नष्ट करने के दौरान फ़ंक्शन को कॉल किया जाता है, तो प्रोग्राम का अपरिभाषित व्यवहार होता है यदि नियंत्रण का प्रवाह गुजरता है पहले नष्ट की गई ब्लॉकचोप ऑब्जेक्ट की परिभाषा के माध्यम से। इसी तरह, व्यवहार को अपरिभाषित किया जाता है यदि ब्लॉक-स्कोप ऑब्जेक्ट का उपयोग अप्रत्यक्ष रूप से किया जाता है (यानी, एक सूचक के माध्यम से) उसके विनाश के बाद।

3. यदि स्थैतिक भंडारण अवधि के साथ किसी वस्तु के आरंभीकरण के पूरा होने पर std :: atexit (“cstdlib”, 18.5 देखें) पर कॉल करने से पहले अनुक्रम किया जाता है, तो std :: atexit को दिए गए फ़ंक्शन का कॉल कॉल से पहले अनुक्रमित होता है। वस्तु के लिए विनाशक। यदि std :: atexit पर कॉल स्थिर स्टोरेज अवधि के साथ किसी ऑब्जेक्ट के आरंभीकरण के पूरा होने से पहले अनुक्रमित है, तो ऑब्जेक्ट के लिए विध्वंसक को कॉल std :: atexit को दिए गए फ़ंक्शन को कॉल करने से पहले अनुक्रमित किया जाता है। अगर std :: atexit को कॉल किया जाता है, तो std :: atexit को कॉल करने से पहले, दूसरे std को दिए गए फ़ंक्शन को कॉल :: atexit कॉल को पहले std :: toexit कॉल को दिए गए फ़ंक्शन को कॉल करने से पहले अनुक्रमित किया जाता है ।

4. यदि सिग्नल हैंडलर (18.10) के भीतर मानक लाइब्रेरी ऑब्जेक्ट या फ़ंक्शन की अनुमति नहीं है, जो स्थिर भंडारण अवधि और एसटीडी के निष्पादन के साथ वस्तुओं के विनाश से पहले नहीं होता है (1.10) :: atexit पंजीकृत फ़ंक्शन (18.5) ), कार्यक्रम में अपरिभाषित व्यवहार होता है। [नोट: यदि स्थैतिक भंडारण अवधि के साथ एक वस्तु का उपयोग होता है जो कि वस्तु के विनाश से पहले नहीं होता है, तो कार्यक्रम में अपरिभाषित व्यवहार होता है। कॉल करने से पहले हर थ्रेड को समाप्त करना :: मुख्य आवश्यकताओं से बाहर निकलना या बाहर निकलना पर्याप्त है, लेकिन इन आवश्यकताओं को पूरा करने के लिए आवश्यक नहीं है। ये आवश्यकताएं थ्रेड प्रबंधकों को स्थैतिक-संग्रहण-अवधि ऑब्जेक्ट के रूप में अनुमति देती हैं। -और नोट]

5. फ़ंक्शन std :: abort ("cstdlib") में घोषित किए गए प्रोग्राम को बिना किसी विध्वंसक को निष्पादित किए बिना और std :: atexit () या std :: at_quick_bit () में दिए गए फ़ंक्शन को कॉल किए बिना समाप्त करता है।
व्याख्या:

  • धागा भंडारण अवधि के साथ वस्तुओं का विनाश उनकी रचना के रिवर्स ऑर्डर में किया जाता है;
  • कड़ाई से उसके बाद, स्थिर भंडारण अवधि वाली वस्तुओं को नष्ट कर दिया जाता है और ऐसी वस्तुओं के निर्माण के रिवर्स ऑर्डर में std :: atexit के साथ पंजीकृत कार्यों के लिए कॉल किए जाते हैं और ऐसे कार्यों को पंजीकृत किया जाता है;
  • थ्रेड स्टोरेज अवधि या स्थिर भंडारण अवधि के साथ नष्ट हुई वस्तु तक पहुंचने के प्रयास में अपरिभाषित व्यवहार होता है। ऐसी वस्तुओं का पुन: प्रारंभ नहीं किया जाता है।

नोट: मानक में वैश्विक चर को "स्थिर भंडारण अवधि के साथ गैर-स्थानीय चर" कहा जाता है। परिणामस्वरूप, यह पता चला है कि सभी वैश्विक चर, सभी सिंगलेटोन (स्थानीय स्टेटिक्स) और सभी कॉल std :: atexit एक ही LIFO कतार में आते हैं क्योंकि वे निर्मित / पंजीकृत होते हैं।

लेख के लिए उपयोगी जानकारी भी गैर-स्थानीय चर [बेसिक.स्टार्ट.इन] की धारा 3.6.2 में निहित है। मैं केवल सबसे महत्वपूर्ण लाता हूं:
स्थैतिक भंडारण अवधि के साथ एक गैर-स्थानीय चर का गतिशील प्रारंभ या तो आदेश दिया गया है या अनियंत्रित है। [...] एकल अनुवाद इकाई के भीतर परिभाषित आरंभीकरण के साथ चर अनुवाद इकाई में उनकी परिभाषाओं के क्रम में आरंभ किया जाएगा।
व्याख्या (अनुभाग के पूर्ण पाठ को ध्यान में रखते हुए): एक अनुवाद इकाई के भीतर वैश्विक चर को घोषणा क्रम में प्रारंभ किया जाता है।

कोड में क्या होगा


लेख में प्रदान किए गए सभी कोड उदाहरण जीथब पर प्रकाशित किए गए हैं।

कोड में तीन परतें होती हैं, जैसे कि विभिन्न लोगों द्वारा लिखित:

  • सिंगलटन;
  • उपयोगिता (सिंगलटन का उपयोग करने वाला वर्ग);
  • उपयोगकर्ता (वैश्विक चर और मुख्य)।

सिंगलटन और यूटिलिटी एक थर्ड-पार्टी लाइब्रेरी की तरह है, और उपयोगकर्ता उपयोगकर्ता है।
यूटिलिटी लेयर को सिंगलटन लेयर से यूजर लेयर को अलग करने के लिए डिज़ाइन किया गया है। उदाहरणों में, उपयोगकर्ता के पास सिंगलटन तक पहुंचने का अवसर है, लेकिन हम ऐसा कार्य करेंगे जैसे कि यह असंभव है।

उपयोगकर्ता पहले सब कुछ सही करता है, और फिर कलाई के एक झटका के साथ सब कुछ टूट जाता है। पहले हम इसे उपयोगिता परत में ठीक करने की कोशिश करते हैं, और यदि यह काम नहीं करता है, तो सिंगलटन परत में।

कोड में, हम लगातार किनारे पर चलेंगे - अब प्रकाश की तरफ, फिर अंधेरे पर। अंधेरे पक्ष में स्विच करना आसान बनाने के लिए, सबसे कठिन मामला चुना गया था - उपयोगिता डिस्ट्रक्टर से एक सिंगलटन तक पहुंचना।

विध्वंसक से कॉल करने का मामला सबसे मुश्किल क्यों है? क्योंकि उपयोगिता विध्वंसक को अनुप्रयोग को कम करने की प्रक्रिया में कहा जा सकता है, जब प्रश्न "सिंगलटन नष्ट हो गया है या अभी तक नहीं" प्रासंगिक हो गया है।

मामला किसी तरह का सिंथेटिक है। व्यवहार में, विध्वंसक से एक सिंगलटन को कॉल करने की आवश्यकता नहीं है। आवश्यकतानुसार भी। उदाहरण के लिए, वस्तुओं का विनाश लॉग करने के लिए।

सिंगलटन के तीन वर्गों का उपयोग किया जाता है:

  • सिंग्लटनक्लासिक - कोई स्मार्ट पॉइंटर्स नहीं। वास्तव में, यह सीधे तौर पर काफी शास्त्रीय नहीं है, लेकिन निश्चित रूप से माना जाने वाले तीनों में सबसे शास्त्रीय है;
  • सिंगलटनशेयरेड - एसटीडी के साथ :: साझा_प्ट्र;
  • सिंगलटनवैक - एसटीडी के साथ :: कमजोर_प्रति।

सभी सिंगलेट्स टेम्प्लेट हैं। टेम्प्लेट पैरामीटर इसका उपयोग इनहेरिट करने के लिए किया जाता है। ज्यादातर उदाहरणों में, उन्हें पेलोड वर्ग द्वारा परिचालित किया जाता है, जो डेटा को std :: set में जोड़ने के लिए एक सार्वजनिक फ़ंक्शन प्रदान करता है।

अधिकांश उदाहरणों में उपयोगिता विध्वंसक वहाँ एक सौ मान भरने की कोशिश करता है। कंसोल के लिए डायग्नोस्टिक आउटपुट का उपयोग सिंगलटन कंस्ट्रक्टर, सिंगलटन डिस्ट्रॉक्टर और उदाहरण () से भी किया जाता है।

इतनी मेहनत क्यों? यह समझना आसान बनाने के लिए कि हम अंधेरे की तरफ हैं। नष्ट किए गए एकल के लिए अपील एक अपरिभाषित व्यवहार है, लेकिन यह किसी भी तरह से बाहरी रूप से प्रकट नहीं हो सकता है। नष्ट किए गए एसटीडी में मूल्यों को भर देना: सेट निश्चित रूप से बाहरी अभिव्यक्तियों की गारंटी नहीं देता है, लेकिन कोई और अधिक विश्वसनीय तरीका नहीं है (वास्तव में, जीसीसी के तहत लिनक्स में क्लासिक सिंगलटन के साथ गलत उदाहरणों में, नष्ट किए गए एसटीडी :: सेट सफलतापूर्वक भर जाता है, और एमएसवीएस के तहत। विंडोज - हैंग हो जाता है)। अपरिभाषित व्यवहार के साथ, कंसोल में आउटपुट नहीं हो सकता है। इसलिए सही उदाहरणों में, हम विध्वंसक के बाद उदाहरण () तक पहुंच की अनुपस्थिति की उम्मीद करते हैं, साथ ही एक दुर्घटना की अनुपस्थिति और एक हैंग की अनुपस्थिति, और गलत लोगों में, या तो इस तरह की अपील की उपस्थिति, या एक दुर्घटना, या एक हैंग, या किसी भी संयोजन में एक बार, या जो भी हो।

क्लासिक सिंगलटन


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


सिंगलटनक्लासिक उदाहरण 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; } 


कंसोल आउटपुट
उदाहरण ()
सिंगलटनक्लासिक ()
उदाहरण ()
~ सिंगलटनक्लासिक ()

उपयोगिता कंस्ट्रक्टर में सिंगलटन को यह सुनिश्चित करने के लिए बुलाती है कि उपयोगिता बनने से पहले सिंगलटन बनाया गया है।

उपयोगकर्ता दो std :: unique_ptr बनाता है: एक खाली, दूसरा जिसमें उपयोगिता है।

निर्माण का क्रम:

- खाली एसटीडी :: unique_ptr।
- सिंगलटन;
- उपयोगिता।

और तदनुसार, विनाश का क्रम:

- उपयोगिता;
- सिंगलटन;
- खाली एसटीडी :: unique_ptr।

यूटिलिटी डिस्ट्रक्टर से सिंगलटन पर कॉल सही है।

सिंगलटनक्लासिक उदाहरण 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; } 


कंसोल आउटपुट
उदाहरण ()
सिंगलटनक्लासिक ()
~ सिंगलटनक्लासिक ()
उदाहरण ()

सृष्टि और विनाश का क्रम संरक्षित है। ऐसा लगता है कि सब कुछ अभी भी है। लेकिन नहीं। खाली यूनीक.स्वाप (यूटिलिटी) को लागू करके, उपयोगकर्ता ने अपरिभाषित व्यवहार किया।

उपयोगकर्ता ने ऐसी मूर्खतापूर्ण बातें क्यों कीं? क्योंकि उन्हें पुस्तकालय की आंतरिक संरचना के बारे में कुछ भी पता नहीं है, जिसने उन्हें एक एकल और उपयोगिता प्रदान की।

और यदि आप पुस्तकालय की आंतरिक संरचना को जानते हैं? ... फिर वैसे भी, वास्तविक कोड में इसे शामिल करना बहुत आसान है। और आपको दर्दनाक बहस से बाहर निकलना होगा, क्योंकि समझने के लिए कि वास्तव में क्या हुआ आसान नहीं होगा।

पुस्तकालय को सही ढंग से उपयोग करने की आवश्यकता क्यों नहीं है? खैर, लिखने के लिए सभी प्रकार के गोदी हैं, उदाहरण ... और क्यों नहीं एक पुस्तकालय बनाया जाए जो खराब करना इतना आसान नहीं है?

सिंग्लटनक्लासिक उदाहरण 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; } 


कंसोल आउटपुट
उदाहरण ()
सिंगलटनक्लासिक ()
उदाहरण ()
उदाहरण ()
~ सिंगलटनक्लासिक ()

स्पष्टीकरण
समस्या केवल तब होती है जब एप्लिकेशन को कम से कम किया जाता है। जब अनुप्रयोग को छोटा किया जाता है, तो पहचानने के लिए उपयोगिता सिखाकर अपरिभाषित व्यवहार को समाप्त किया जा सकता है। ऐसा करने के लिए, हमने std :: साझा_ptr प्रकार का फ्लैग_स्ट्रॉन्ग वैरिएबल का उपयोग किया, जिसमें थ्रेड स्टोरेज की अवधि क्वालिफायर है (ऊपर आलेख में मानक से अंश देखें) - यह एक स्टेटिक की तरह है, लेकिन केवल तब नष्ट होता है जब स्टैटिक्स नष्ट होने से पहले स्टैटिक्स के किसी भी नष्ट होने से पहले समाप्त हो जाता है । सिंगलटन। फ्लैग_स्ट्रॉन्ग वेरिएबल संपूर्ण स्ट्रीम के लिए एक है, और यूटिलिटी का प्रत्येक उदाहरण इसकी कमजोर कॉपी को संग्रहीत करता है।

संकीर्ण अर्थ में, समाधान को हैक कहा जा सकता है, क्योंकि यह अप्रत्यक्ष और गैर-स्पष्ट है। इसके अलावा, यह बहुत जल्दी चेतावनी देता है, और कभी-कभी (एक बहु-थ्रेडेड एप्लिकेशन में) आम तौर पर गलत चेतावनी देता है। लेकिन एक व्यापक अर्थ में, यह हैक नहीं है, बल्कि मानक गुणों द्वारा पूरी तरह से परिभाषित एक समाधान है - नुकसान और फायदे दोनों।

SingletonShared


चलिए std :: 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; } }; 


एआई-ए-आई, नए ऑपरेटर को आधुनिक कोड में उपयोग नहीं किया जाना चाहिए, इसके बजाय std :: make_sared की आवश्यकता है! और इसे सिंगलटन के निजी कंस्ट्रक्टर द्वारा रोका जाता है।

हा! मुझे भी एक समस्या है! Decdare std :: make_sared एक सिंगलटन फ्रीड! ... और एंटीपार्टर्न पब्लिकमोरोज़ोव की एक विविधता प्राप्त करें: एक ही एसटीडी :: मेक_शेयर का उपयोग करके, एकल के अतिरिक्त उदाहरण बनाना संभव होगा जो आर्किटेक्चर द्वारा प्रदान नहीं किए गए हैं।

सिंगलटनशेयरिंग उदाहरण 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; } 


कंसोल आउटपुट
उदाहरण ()
सिंगलटनशेयर ()
उदाहरण ()
~ सिंगलटनशेयरिंग ()

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


कंसोल आउटपुट
उदाहरण ()
सिंगलटनशेयर ()
~ सिंगलटनशेयरिंग ()
उदाहरण ()

सिंगलटन उदाहरण 3


और अब हम क्लासिक्स से उदाहरण संख्या 3 की तुलना में इस समस्या को ठीक करने का प्रयास करेंगे।
समाधान स्पष्ट है: आपको केवल एक प्रतिलिपि std :: share_ptr उपयोगिता में सिंगलटन द्वारा लौटाकर सिंगलटन के जीवन का विस्तार करने की आवश्यकता है। और सिंगलटन के साथ पूरा होने वाला यह समाधान, खुले स्रोतों में व्यापक रूप से दोहराया गया है।

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


कंसोल आउटपुट
उदाहरण ()
सिंगलटनशेयर ()
~ सिंगलटनशेयरिंग ()

और अब, ध्यान, सवाल यह है: क्या आप वास्तव में एक सिंगलटन के जीवन का विस्तार करना चाहते थे?
या क्या आप अनिश्चित व्यवहार से छुटकारा चाहते हैं, और सतह पर झूठ बोलने के तरीके के रूप में जीवन विस्तार का चयन करते हैं?

साधनों द्वारा लक्ष्यों के प्रतिस्थापन के रूप में सैद्धांतिक गलतफहमी गतिरोध (या चक्रीय संदर्भ - जिसे आप चाहते हैं उसे कॉल करें) का जोखिम होता है।

हाँ, नूऊऊऊऊ, यह आपको इतना कठिन प्रयास करना है! आपको इतने लंबे समय के साथ आना होगा, और आप निश्चित रूप से इसे दुर्घटना से नहीं करेंगे!

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() // To ensure that singleton will be constucted before utility : m_singleton(SingletonShared<CallbackPayload>::instance()) { std::cout << "SharedSingleThreadedUtility()" << std::endl; } ~SharedSingleThreadedUtility() { std::cout << "~SharedSingleThreadedUtility()" << std::endl; } void setCallback(std::function<void()> &&fn) { if ( m_singleton ) m_singleton->setCallback(std::move(fn)); } private: // A copy of smart pointer, not a reference std::shared_ptr<SingletonShared<CallbackPayload>> m_singleton; }; int main() { auto utility = std::make_shared<SharedSingleThreadedUtility>(); auto something = std::make_shared<SomethingWithVeryImportantDestructor>(); // lambda with "utility" and "something" captured utility->setCallback( [utility, something](){} ); return 0; } 


कंसोल आउटपुट
उदाहरण ()
सिंगलटनशेयर ()
SharedSingleThreadedUtility ()
समथिंगवर्थइम्पोर्टेंटडिस्ट्रक्टर ()

एक सिंगलटन बनाया गया था।

एक उपयोगिता बनाई गई है।

कुछ एस-वेरी-महत्वपूर्ण-विनाशकारी बनाया गया था (मैंने इसे डराने के लिए जोड़ा था , क्योंकि इंटरनेट पर "अच्छी तरह से" जैसे पोस्ट हैं, सिंगलटन डिस्ट्रॉक्टर को नहीं बुलाया जाएगा, इसलिए यह क्या है, यह हर समय मौजूद होना चाहिए कार्यक्रम ”)।

लेकिन इनमें से किसी भी वस्तु के लिए कोई विध्वंसक नहीं कहा गया!

किस वजह से? साधनों द्वारा लक्ष्यों के प्रतिस्थापन के कारण।

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


खुले स्रोतों में सिंगलटन का ऐसा संशोधन, यदि दिया गया है, तो निश्चित रूप से अक्सर नहीं होता है। मैं कुछ अजीब तरह के स्टैड के साथ बाहर घूमता हुआ मिला: कमजोर_प्रति, जिसका उपयोग किया जा रहा है, जो, ऐसा लगता है, एक सिंगलटन के जीवन को लम्बा करने के लिए उपयोगिता से अधिक कुछ नहीं है:


वह विकल्प जो मैं प्रस्तावित करता हूं, जब सिंगलटन और उपयोगिता परतों में सही तरीके से लागू किया जाता है:

  • उपर्युक्त उदाहरणों में वर्णित उपयोगकर्ता परत में कार्यों के खिलाफ सुरक्षा, गतिरोध को रोकता है;
  • Classic_Example3_correct में थ्रेड_लोकल एप्लिकेशन से अधिक सटीक रूप से एप्लिकेशन फोल्डिंग के क्षण को निर्धारित करता है, अर्थात आपको किनारे के करीब आने की अनुमति देता है;
  • मैं साधनों के साथ लक्ष्यों को प्रतिस्थापित करने की सैद्धांतिक समस्या से ग्रस्त नहीं हूँ (मुझे नहीं पता कि गतिरोध के अलावा अन्य कोई ठोस चीज़ इस सैद्धांतिक समस्या से प्रकट हो सकती है)।

हालांकि, एक खामी है: एक सिंगलटन के जीवन का विस्तार अभी भी इसे किनारे के करीब आने की अनुमति दे सकता है

सिंगलटन उदाहरण 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 की आवश्यकता क्यों है, क्योंकि कोई भी व्यक्ति सिंगलटन के रूप में सिंगलटन का उपयोग करने की उपयोगिता को परेशान नहीं करता है? हां, कोई परेशान नहीं करता। और यहां तक ​​कि कोई भी सिंगलटन शेल्ड के रूप में सिंगलटनवेक का उपयोग करने की उपयोगिता को परेशान नहीं करता है। लेकिन उनका उपयोग अपने इच्छित उद्देश्य के लिए करना अन्य उद्देश्यों के लिए उपयोग करने की तुलना में थोड़ा आसान है।

सिंगलटन उदाहरण 2


Shared_Example4_incorrect के समान, लेकिन इस मामले में केवल गतिरोध नहीं होता है।

Weak_Example2_correct.cpp
 #include "SingletonWeak.h" #include "CallbackPayload.h" #include "SomethingWithVeryImportantDestructor.h" class WeakSingleThreadedUtility { public: WeakSingleThreadedUtility() // To ensure that singleton will be constucted before utility : m_weak(SingletonWeak<CallbackPayload>::instance()) { std::cout << "WeakSingleThreadedUtility()" << std::endl; } ~WeakSingleThreadedUtility() { std::cout << "~WeakSingleThreadedUtility()" << std::endl; } void setCallback(std::function<void()> &&fn) { if ( auto strong = m_weak.lock() ) strong->setCallback(std::move(fn)); } private: // A weak copy of smart pointer, not a reference std::weak_ptr<SingletonWeak<CallbackPayload>> m_weak; }; int main() { auto utility = std::make_shared<WeakSingleThreadedUtility>(); auto something = std::make_shared<SomethingWithVeryImportantDestructor>(); // lambda with "utility" and "something" captured utility->setCallback( [utility, something](){} ); return 0; } 


कंसोल आउटपुट
उदाहरण ()
सिंगलटनवॉक ()
कमज़ोरपन
समथिंगवर्थइम्पोर्टेंटडिस्ट्रक्टर ()
~ सिंगलटनवेक ()
~ समथिंगवर्थइम्पोर्टेंटडिस्ट्रक्टर ()
~ कमज़ोर थ्रेडेड यूटिलिटी ()

एक निष्कर्ष के बजाय


और क्या, एक सिंगलटन का ऐसा संशोधन अपरिभाषित व्यवहार को समाप्त कर देगा? मैंने वादा किया कि कोई सुखद अंत नहीं होगा। निम्नलिखित उदाहरण बताते हैं कि उपयोगकर्ता परत में कुशल तोड़फोड़ की कार्रवाई एक सिंगलटन के साथ सही सोचा-समझा पुस्तकालय को भी नष्ट कर सकती है (लेकिन हमें यह स्वीकार करना चाहिए कि यह शायद ही दुर्घटना से हो सकता है)।

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


कंसोल आउटपुट
उदाहरण ()
सिंगलटनशेयर ()
~ सिंगलटनशेयरिंग ()
उदाहरण ()

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


कंसोल आउटपुट
उदाहरण ()
सिंगलटनवॉक ()
~ सिंगलटनवेक ()
उदाहरण ()

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


All Articles