Der Artikel beschreibt die Ursachen und Methoden zur Vermeidung von undefiniertem Verhalten beim Zugriff auf einen Singleton in modernem C ++. Beispiele für Single-Threaded-Code werden bereitgestellt. Nichts compilerspezifisches, alles in Übereinstimmung mit dem Standard.
Einführung
Zu Beginn empfehle ich Ihnen, andere Artikel über Singleton auf Habré zu lesen:
Drei Zeitalter des Singleton-MustersSingleton und allgemeine Instanzen3 Möglichkeiten, das Prinzip der Einzelverantwortung zu brechenSingleton - Muster oder Antimuster?Verwenden des Singleton-MustersUnd schließlich ein Artikel, der das gleiche Thema berührte, aber durchrutschte (schon allein deshalb, weil die Nachteile und Einschränkungen nicht berücksichtigt wurden):
tialisierte Objekte (dh Objekte
Singleton und ObjektlebensdauerWeiter:
- Dies ist kein Artikel über die architektonischen Eigenschaften von Singleton.
- Dies ist kein Artikel, „wie man aus einem schrecklichen und schrecklichen Singleton einen weißen und flauschigen Singleton macht“;
- Dies ist keine Singleton-Kampagne.
- es ist kein Kreuzzug gegen Singleton;
- Dies ist kein Happy-End-Artikel.
In diesem Artikel geht es um einen sehr wichtigen, aber immer noch technischen Aspekt der Verwendung von Singleton in modernem C ++. Das Hauptaugenmerk in dem Artikel liegt auf dem Moment der Zerstörung des Singletons. In den meisten Quellen ist das Thema Zerstörung nur unzureichend bekannt. Normalerweise liegt der Schwerpunkt auf dem Moment, in dem der Singleton erstellt wurde, und über Zerstörung sagt er bestenfalls etwas wie "in umgekehrter Reihenfolge zerstört".
Ich bitte Sie, den Umfang des Artikels in den Kommentaren zu befolgen, insbesondere das Singleton-Muster nicht gegen das Singleton-Antipattern-Holivar anzuordnen.Also lass uns gehen.
Was der Standard sagt
Die Zitate stammen aus dem endgültigen Entwurf von C ++ 14, N3936, as verfügbare C ++ 17-Entwürfe werden nicht als "endgültig" markiert.
Ich gebe den wichtigsten Abschnitt in seiner Gesamtheit. Wichtige Orte werden von mir hervorgehoben.
3.6.3 Kündigung [basic.start.term]
1. Destruktoren (12.4) für initialisierte Objekte (dh Objekte, deren Lebensdauer (3.8) begonnen hat) mit statischer Speicherdauer werden als Ergebnis der Rückkehr von main und als Ergebnis des Aufrufs von std :: exit (18.5) aufgerufen. Destruktoren für initialisierte Objekte mit Thread-Speicherdauer innerhalb eines bestimmten Threads werden als Ergebnis der Rückkehr von der Anfangsfunktion dieses Threads und als Ergebnis des Aufrufs von std :: exit durch diesen Thread aufgerufen. Die Vervollständigungen der Destruktoren für alle initialisierten Objekte mit Thread-Speicherdauer innerhalb dieses Threads werden vor der Initiierung der Destruktoren eines Objekts mit statischer Speicherdauer sequenziert. Wenn der Abschluss des Konstruktors oder die dynamische Initialisierung eines Objekts mit Thread-Speicherdauer vor dem eines anderen sequenziert wird, wird der Abschluss des Destruktors des zweiten vor der Initiierung des Destruktors des ersten sequenziert. Wenn der Abschluss des Konstruktors oder die dynamische Initialisierung eines Objekts mit statischer Speicherdauer vor dem eines anderen sequenziert wird, wird der Abschluss des Destruktors des zweiten vor der Initiierung des Destruktors des ersten sequenziert. [Hinweis: Diese Definition ermöglicht die gleichzeitige Zerstörung. –Ende Hinweis] Wenn ein Objekt statisch initialisiert wird, wird das Objekt in derselben Reihenfolge zerstört, als ob das Objekt dynamisch initialisiert worden wäre. Bei einem Objekt vom Typ Array oder Klasse werden alle Unterobjekte dieses Objekts zerstört, bevor ein Blockbereichsobjekt mit statischer Speicherdauer, das während der Erstellung der Unterobjekte initialisiert wurde, zerstört wird. Wenn die Zerstörung eines Objekts mit statischer oder Thread-Speicherdauer über eine Ausnahme beendet wird, wird std :: terminate (15.5.1) aufgerufen.
2. Wenn eine Funktion ein Block-Scope-Objekt mit statischer oder Thread-Speicherdauer enthält, das zerstört wurde, und die Funktion während der Zerstörung eines Objekts mit statischer oder Thread-Speicherdauer aufgerufen wird, hat das Programm ein undefiniertes Verhalten, wenn der Steuerungsfluss erfolgreich ist durch die Definition des zuvor zerstörten Blockscope-Objekts. Ebenso ist das Verhalten undefiniert, wenn das Block-Scope-Objekt nach seiner Zerstörung indirekt (dh über einen Zeiger) verwendet wird.
3. Wenn der Abschluss der Initialisierung eines Objekts mit statischer Speicherdauer vor einem Aufruf von std :: atexit (siehe "cstdlib", 18.5) sequenziert wird, wird der Aufruf der an std :: atexit übergebenen Funktion vor dem Aufruf sequenziert an den Destruktor für das Objekt. Wenn ein Aufruf von std :: atexit vor Abschluss der Initialisierung eines Objekts mit statischer Speicherdauer sequenziert wird, wird der Aufruf des Destruktors für das Objekt vor dem Aufruf der an std :: atexit übergebenen Funktion sequenziert. Wenn ein Aufruf von std :: atexit vor einem weiteren Aufruf von std :: atexit sequenziert wird, wird der Aufruf der an den zweiten std :: atexit-Aufruf übergebenen Funktion vor dem Aufruf der an den ersten std :: atexit-Aufruf übergebenen Funktion sequenziert .
4. Wenn ein Standardbibliotheksobjekt oder eine Standardbibliotheksfunktion innerhalb der Signalhandler (18.10) nicht zulässig ist, erfolgt dies nicht vor (1.10) Abschluss der Zerstörung von Objekten mit statischer Speicherdauer und Ausführung der registrierten std :: atexit-Funktionen (18.5) ) hat das Programm ein undefiniertes Verhalten. [Hinweis: Wenn ein Objekt mit statischer Speicherdauer verwendet wird, das nicht vor der Zerstörung des Objekts auftritt, weist das Programm ein undefiniertes Verhalten auf. Das Beenden jedes Threads vor einem Aufruf von std :: exit oder dem Beenden von main ist ausreichend, aber nicht erforderlich, um diese Anforderungen zu erfüllen. Diese Anforderungen ermöglichen Thread-Manager als Objekte mit statischer Speicherdauer. - Endnote]
5. Durch Aufrufen der in „cstdlib“ deklarierten Funktion std :: abort () wird das Programm beendet, ohne Destruktoren auszuführen und ohne die an std :: atexit () oder std :: at_quick_exit () übergebenen Funktionen aufzurufen.
Interpretation:
- Die Zerstörung von Objekten mit Thread-Speicherdauer erfolgt in umgekehrter Reihenfolge ihrer Erstellung.
- Unmittelbar danach werden Objekte mit statischer Speicherdauer zerstört und Funktionen, die bei std :: atexit registriert sind, in umgekehrter Reihenfolge aufgerufen, in der solche Objekte erstellt und solche Funktionen registriert werden.
- Ein Versuch, auf ein zerstörtes Objekt mit Thread-Speicherdauer oder statischer Speicherdauer zuzugreifen, enthält undefiniertes Verhalten. Eine Neuinitialisierung solcher Objekte ist nicht vorgesehen.
Hinweis: Globale Variablen im Standard werden als "nicht lokale Variable mit statischer Speicherdauer" bezeichnet. Als Ergebnis stellt sich heraus, dass alle globalen Variablen, alle Singletones (lokale Statik) und alle Aufrufe von std :: atexit beim Erstellen / Registrieren in eine einzige LIFO-Warteschlange fallen.
Informationen, die für den Artikel nützlich sind, finden Sie auch in Abschnitt
3.6.2 Initialisierung nicht lokaler Variablen [basic.start.init] . Ich bringe nur das Wichtigste mit:
Die dynamische Initialisierung einer nicht lokalen Variablen mit statischer Speicherdauer ist entweder geordnet oder ungeordnet. [...] Variablen mit geordneter Initialisierung, die in einer einzelnen Übersetzungseinheit definiert sind, werden in der Reihenfolge ihrer Definitionen in der Übersetzungseinheit initialisiert.
Interpretation (unter Berücksichtigung des vollständigen Textes des Abschnitts): Globale Variablen innerhalb einer Übersetzungseinheit werden in der Deklarationsreihenfolge initialisiert.
Was wird im Code sein
Alle im Artikel bereitgestellten Codebeispiele werden auf dem
Github veröffentlicht .
Der Code besteht aus drei Ebenen, als ob er von verschiedenen Personen geschrieben wurde:
- Singleton;
- Dienstprogramm (Klasse mit Singleton);
- Benutzer (globale Variablen und main).
Singleton und das Dienstprogramm sind wie eine Bibliothek eines Drittanbieters, und der Benutzer ist der Benutzer.
Die Utility-Schicht dient dazu, die Benutzerschicht von der Singleton-Schicht zu isolieren. In den Beispielen hat der Benutzer die Möglichkeit, auf den Singleton zuzugreifen, aber wir werden so tun, als ob dies unmöglich wäre.
Der Benutzer macht zuerst alles richtig und dann bricht mit einem Handgriff alles. Zuerst versuchen wir, es in der Utility-Schicht zu beheben, und wenn es nicht funktioniert, dann in der Singleton-Schicht.
Im Code werden wir ständig am Rand entlang gehen - jetzt auf der hellen Seite, dann auf der dunklen Seite. Um den Wechsel zur dunklen Seite zu erleichtern, wurde der schwierigste Fall ausgewählt - der Zugriff auf einen Singleton über den Utility-Destruktor.
Warum ist der Anruf vom Destruktor am schwierigsten? Da der Utility-Destruktor beim Minimieren der Anwendung aufgerufen werden kann, wird die Frage "Wurde der Singleton zerstört oder noch nicht" relevant.
Der Fall ist eine Art synthetischer. In der Praxis sind Aufrufe eines Singletons vom Destruktor nicht erforderlich. Auch nach Bedarf. Zum Beispiel, um die Zerstörung von Objekten zu protokollieren.
Es werden drei Klassen von Singleton verwendet:
- SingletonClassic - keine intelligenten Zeiger. Tatsächlich ist es nicht direkt ganz klassisch, aber definitiv das klassischste unter den drei betrachteten;
- SingletonShared - mit std :: shared_ptr;
- SingletonWeak - mit std :: schwach_ptr.
Alle Singletones sind Vorlagen. Der Template-Parameter wird verwendet, um davon zu erben. In den meisten Beispielen werden sie von der Payload-Klasse parametrisiert, die eine öffentliche Funktion zum Hinzufügen von Daten zu std :: set bereitstellt.
In den meisten Beispielen versucht der Utility-Destruktor, dort hundert Werte einzugeben. Die Diagnoseausgabe an die Konsole wird auch vom Singleton-Konstruktor, dem Singleton-Destruktor und instance () verwendet.
Warum so schwer? Um leichter zu verstehen, dass wir auf der dunklen Seite sind. Der Appell an den zerstörten Singleton ist ein undefiniertes Verhalten, das sich jedoch möglicherweise nicht extern manifestiert. Das Einfügen von Werten in das zerstörte std :: set garantiert sicherlich auch keine externen Manifestationen, aber es gibt keinen zuverlässigeren Weg (tatsächlich wird in GCC unter Linux in falschen Beispielen mit dem klassischen Singleton das zerstörte std :: set erfolgreich gestopft und in MSVS unter Windows - hängt). Bei undefiniertem Verhalten erfolgt die Ausgabe an die Konsole möglicherweise
nicht . In den richtigen Beispielen erwarten wir also das Fehlen eines Zugriffs auf instance () nach dem Destruktor sowie das Fehlen eines Absturzes und das Fehlen eines Hangs und in den falschen entweder das Vorhandensein eines solchen Aufrufs oder eines Absturzes oder eines Hängens oder alles auf einmal in einer beliebigen Kombination oder was auch immer.
Klassischer Singleton
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 Beispiel 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; }
Konsolenausgabeinstance ()
SingletonClassic ()
instance ()
~ SingletonClassic ()
Das Dienstprogramm ruft den Singleton im Konstruktor auf, um sicherzustellen, dass der Singleton erstellt wird, bevor das Dienstprogramm erstellt wird.
Der Benutzer erstellt zwei std :: unique_ptr: eine leere, die zweite enthält das Dienstprogramm.
Die Reihenfolge der Schöpfung:
- leer std :: unique_ptr.
- Singleton;
- Dienstprogramm.
Und dementsprechend die Reihenfolge der Zerstörung:
- Dienstprogramm;
- Singleton;
- leer std :: unique_ptr.
Der Aufruf vom Utility-Destruktor an den Singleton ist korrekt.
SingletonClassic Beispiel 2
Alles ist gleich, aber der Benutzer hat es genommen und alles mit einer Zeile ruiniert.
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; }
Konsolenausgabeinstance ()
SingletonClassic ()
~ SingletonClassic ()
instance ()
Die Ordnung der Schöpfung und Zerstörung bleibt erhalten. Es scheint, dass alles still ist. Aber nein. Durch Aufrufen von emptyUnique.swap (UtilityUnique) hat der Benutzer ein undefiniertes Verhalten begangen.
Warum hat der Benutzer so dumme Dinge getan? Weil er nichts über die interne Struktur der Bibliothek weiß, die ihm einen Singleton und ein Dienstprogramm verschaffte.
Und wenn Sie die interne Struktur der Bibliothek kennen? ... dann ist es in echtem Code sehr einfach, sich zu engagieren. Und du musst durch schmerzhaftes Debag raus, weil zu verstehen, was genau passiert ist, wird nicht einfach sein.
Warum muss die Bibliothek nicht korrekt verwendet werden? Nun, es gibt alle Arten von Docks zu schreiben, Beispiele ... Und warum nicht eine Bibliothek erstellen, die nicht so einfach zu verderben ist?
SingletonClassic Beispiel 3
Während der Vorbereitung des Artikels für mehrere Tage glaubte ich, dass es unmöglich war, unbestimmtes Verhalten aus dem vorherigen Beispiel in der Utility-Schicht zu entfernen, und die Lösung war nur in der Singleton-Schicht verfügbar. Im Laufe der Zeit wurde jedoch eine Lösung gefunden.
Bevor Sie die Spoiler mit dem Code und der Erklärung öffnen, empfehle ich dem Leser, selbst einen Ausweg aus der Situation zu finden (nur in der Utility-Schicht!). Ich schließe nicht aus, dass es bessere Lösungen gibt.
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; }
Konsolenausgabeinstance ()
SingletonClassic ()
instance ()
instance ()
~ SingletonClassic ()
ErklärungDas Problem tritt nur auf, wenn die Anwendung minimiert wird. Undefiniertes Verhalten kann beseitigt werden, indem dem Dienstprogramm beigebracht wird, zu erkennen, wann die Anwendung minimiert wird. Zu diesem Zweck haben wir eine Variable flag_strong vom Typ std :: shared_ptr verwendet, die über ein Qualifikationsmerkmal für die Thread-Speicherdauer verfügt (siehe Auszüge aus dem Standard im obigen Artikel). Dies ist wie eine statische Variable, wird jedoch nur zerstört, wenn der aktuelle Thread endet, bevor eine der Statiken zerstört wird , auch vor der Zerstörung Singleton. Die Variable flag_strong ist eine für den gesamten Stream, und jede Instanz des Dienstprogramms speichert ihre schwache Kopie.
Im engeren Sinne kann die Lösung als Hack bezeichnet werden, weil es ist indirekt und nicht offensichtlich. Außerdem warnt es zu früh und manchmal (in einer Multithread-Anwendung) warnt es im Allgemeinen falsch. Im weitesten Sinne ist dies jedoch kein Hack, sondern eine Lösung, die vollständig durch die Standardeigenschaften definiert ist - sowohl Nachteile als auch Vorteile.
Singletonshared
Fahren wir mit einem modifizierten Singleton fort, der auf std :: shared_ptr basiert.
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, der neue Operator sollte nicht in modernem Code verwendet werden, stattdessen wird std :: make_shared benötigt! Und dies wird durch den privaten Konstruktor des Singletons verhindert.
Ha! Ich habe auch ein Problem! Erkläre std :: make_shared als Singleton-Freund! ... und erhalten Sie eine Variation des Antipatterns PublicMorozov: Mit demselben std :: make_shared können zusätzliche Instanzen des Singletons erstellt werden, die von der Architektur nicht bereitgestellt werden.
SingletonShared-Beispiele 1 und 2
Entsprechen vollständig den Beispielen Nr. 1 und 2 für die klassische Version. Wesentliche Änderungen wurden nur an der Singleton-Schicht vorgenommen, das Dienstprogramm blieb im Wesentlichen gleich. Genau wie in den Beispielen mit dem klassischen Singleton ist Beispiel 1 korrekt und Beispiel 2 zeigt undefiniertes Verhalten.
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; }
Konsolenausgabeinstance ()
SingletonShared ()
instance ()
~ 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; }
Konsolenausgabeinstance ()
SingletonShared ()
~ SingletonShared ()
instance ()
SingletonShared-Beispiel 3
Und jetzt werden wir versuchen, dieses Problem besser zu beheben als im Beispiel Nummer 3 der Klassiker.
Die Lösung liegt auf der Hand: Sie müssen lediglich die Lebensdauer des Singletons verlängern, indem Sie eine vom Singleton zurückgegebene Kopie von std :: shared_ptr im Dienstprogramm speichern. Und diese Lösung, zusammen mit SingletonShared, wurde in Open Source weitgehend repliziert.
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; }
Konsolenausgabeinstance ()
SingletonShared ()
~ SingletonShared ()
Und nun, Aufmerksamkeit, die Frage ist:
Wollten Sie wirklich das Leben eines Singletons verlängern?Oder wollten Sie unbestimmtes Verhalten loswerden und die Verlängerung des Lebens als einen Weg wählen, der an der Oberfläche liegt?
Theoretische Unkorrektheit in Form der Substitution von Zielen durch Mittel führt zum Risiko eines Deadlocks (oder einer zyklischen Referenz - nennen Sie es so, wie Sie es wollen).
Ja nuuuuuu, so musst du es versuchen !? Sie müssen sich so viel Zeit einfallen lassen, und Sie werden es sicherlich nicht zufällig tun!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()
Konsolenausgabeinstance ()
SingletonShared ()
SharedSingleThreadedUtility ()
SomethingWithVeryImportantDestructor ()
Ein Singleton wurde erstellt.
Ein Dienstprogramm wurde erstellt.
Es wurde etwas S-Very-Important-Destructor erstellt (ich habe dies zur Einschüchterung hinzugefügt, da es im Internet Beiträge wie "Nun, der Singleton-Destruktor wird nicht aufgerufen, also was ist, er muss die ganze Zeit existieren." Programme ”).
Aber für keines dieser Objekte wurde ein Zerstörer gerufen!
Wegen was? Aufgrund der Substitution von Toren durch Mittel.
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; } };
Eine solche Modifikation des Singletons in Open Source ist, wenn gegeben, sicherlich nicht oft. Ich bin auf einige seltsame Optionen gestoßen, die mit einem std :: schwach_ptr auf den Kopf gestellt wurden, der anscheinend verwendet wird und dem Dienstprogramm anscheinend nichts weiter bietet, als die Lebensdauer eines Singletons zu verlängern:
Die von mir vorgeschlagene Option bei korrekter Anwendung in Singleton- und Utility-Ebenen:
- schützt vor Aktionen in der in den obigen Beispielen beschriebenen Benutzerebene, einschließlich Verhinderung von Deadlocks;
- bestimmt den Moment der Anwendungsfaltung genauer als die thread_local-Anwendung in Classic_Example3_correct, d.h. ermöglicht es Ihnen, näher an den Rand zu kommen;
- Ich leide nicht unter dem theoretischen Problem, Ziele durch Mittel zu ersetzen (ich weiß nicht, ob aus diesem theoretischen Problem etwas anderes als ein Deadlock hervorgehen kann).
Es gibt jedoch einen Nachteil: Wenn Sie die Lebensdauer eines Singletons verlängern, kann er
immer noch näher an den Rand kommen.
SingletonWeak Beispiel 1
Ähnlich wie bei 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; }
Konsolenausgabeinstance ()
SingletonWeak ()
~ SingletonWeak ()
Warum brauchen wir SingletonWeak, weil niemand das Dienstprogramm stört, SingletonShared als SingletonWeak zu verwenden? Ja, niemand stört. Und selbst niemand stört das Dienstprogramm, SingletonWeak als SingletonShared zu verwenden. Die Verwendung für den beabsichtigten Zweck ist jedoch etwas einfacher als die Verwendung für andere Zwecke.
SingletonWeak Beispiel 2
Ähnlich wie Shared_Example4_incorrect, aber in diesem Fall tritt nur kein Deadlock auf.
Weak_Example2_correct.cpp #include "SingletonWeak.h" #include "CallbackPayload.h" #include "SomethingWithVeryImportantDestructor.h" class WeakSingleThreadedUtility { public: WeakSingleThreadedUtility()
Konsolenausgabeinstance ()
SingletonWeak ()
WeakSingleThreadedUtility ()
SomethingWithVeryImportantDestructor ()
~ SingletonWeak ()
~ SomethingWithVeryImportantDestructor ()
~ WeakSingleThreadedUtility ()
Anstelle einer Schlussfolgerung
Und was, eine solche Modifikation eines Singletons wird undefiniertes Verhalten beseitigen? Ich habe versprochen, dass es kein Happy End geben wird. Die folgenden Beispiele zeigen, dass geschickte Sabotage in der Benutzerebene sogar die richtige durchdachte Bibliothek mit einem Singleton zerstören kann (aber wir müssen zugeben, dass
dies kaum zufällig möglich ist).
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; }
Konsolenausgabeinstance ()
SingletonShared ()
~ SingletonShared ()
instance ()
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; }
Konsolenausgabeinstance ()
SingletonWeak ()
~ SingletonWeak ()
instance ()