L'article décrit les causes et les méthodes permettant d'éviter un comportement indéfini lors de l'accès à un singleton en c ++ moderne. Des exemples de code à thread unique sont fournis. Rien de spécifique au compilateur, tout est conforme à la norme.
Présentation
Pour commencer, je vous recommande de lire d'autres articles sur singleton sur Habré:
Trois âges de motif SingletonSingleton et instances communes3 façons de briser le principe de responsabilité uniqueSingleton - motif ou antipattern?Utilisation du motif singletonEt, enfin, un article qui a abordé le même sujet, mais qui s'est glissé (ne serait-ce que parce que les inconvénients et les limites n'ont pas été pris en compte):
des objets spécialisés (c'est-à -dire des objets
Singleton et durée de vie des objetsSuivant:
- ce n'est pas un article sur les propriétés architecturales de singleton;
- ce n'est pas un article «comment faire un singleton blanc et moelleux d'un singleton terrible et terrible»;
- ce n'est pas une campagne singleton;
- ce n'est pas une croisade contre singleton;
- ce n'est pas un article de fin heureuse.
Cet article traite d'un aspect très important, mais toujours technique, de l'utilisation de singleton dans le C ++ moderne. La principale attention de l'article est accordée au moment de la destruction du singleton, comme dans la plupart des sources, le problème de la destruction est mal divulgué. Habituellement, l'accent est mis sur le moment où le singleton a été créé, et sur la destruction, au mieux, il dit quelque chose comme «détruit dans l'ordre inverse».
Je vous demanderai de suivre la portée de l'article dans les commentaires, en particulier de ne pas organiser l'holivar "modèle singleton vs singleton-antipattern".Alors allons-y.
Ce que dit la norme
Les citations sont tirées du projet final N3936 de C ++ 14, comme les versions C ++ 17 disponibles ne sont pas marquées comme «finales».
Je donne la section la plus importante dans son intégralité. Des endroits importants sont mis en évidence par moi.
3.6.3 Résiliation [basic.start.term]
1. Les destructeurs (12.4) pour les objets initialisés (c'est-à -dire les objets dont la durée de vie (3.8) a commencé) avec une durée de stockage statique sont appelés suite au retour de main et suite à l'appel à std :: exit (18.5). Les destructeurs pour les objets initialisés avec une durée de stockage de thread dans un thread donné sont appelés suite au retour de la fonction initiale de ce thread et suite à l'appel de std :: exit par ce thread. La fin des destructeurs pour tous les objets initialisés avec une durée de stockage de thread dans ce thread est séquencée avant le lancement des destructeurs de tout objet avec une durée de stockage statique. Si l'achèvement du constructeur ou l'initialisation dynamique d'un objet avec la durée de stockage des threads est séquencé avant celui d'un autre, l'achèvement du destructeur du second est séquencé avant l'initiation du destructeur du premier. Si l'achèvement du constructeur ou l'initialisation dynamique d'un objet avec une durée de stockage statique est séquencé avant celui d'un autre, l'achèvement du destructeur du second est séquencé avant l'initiation du destructeur du premier. [Remarque: Cette définition permet la destruction simultanée. –Fin note] Si un objet est initialisé statiquement, l'objet est détruit dans le même ordre que si l'objet était initialisé dynamiquement. Pour un objet de type tableau ou classe, tous les sous-objets de cet objet sont détruits avant la destruction de tout objet de portée de bloc avec une durée de stockage statique initialisée lors de la construction des sous-objets. Si la destruction d'un objet avec une durée de stockage statique ou de thread se termine via une exception, std :: terminate est appelée (15.5.1).
2. Si une fonction contient un objet de portée de bloc de durée de stockage statique ou de thread qui a été détruit et que la fonction est appelée pendant la destruction d'un objet avec une durée de stockage statique ou de thread, le programme a un comportement indéfini si le flux de contrôle passe à travers la définition de l'objet blockscope précédemment détruit. De même, le comportement n'est pas défini si l'objet de portée de bloc est utilisé indirectement (c'est-à -dire via un pointeur) après sa destruction.
3. Si l'achèvement de l'initialisation d'un objet avec une durée de stockage statique est séquencé avant un appel à std :: atexit (voir «cstdlib», 18.5), l'appel à la fonction passée à std :: atexit est séquencé avant l'appel au destructeur de l'objet. Si un appel à std :: atexit est séquencé avant la fin de l'initialisation d'un objet avec une durée de stockage statique, l'appel au destructeur de l'objet est séquencé avant l'appel à la fonction passé à std :: atexit. Si un appel à std :: atexit est séquencé avant un autre appel à std :: atexit, l'appel à la fonction passé au deuxième appel std :: atexit est séquencé avant l'appel à la fonction passé au premier appel std :: atexit .
4. S'il y a une utilisation d'un objet ou d'une fonction de bibliothèque standard non autorisée dans les gestionnaires de signaux (18.10) qui ne se produit pas avant (1.10) la fin de la destruction des objets avec une durée de stockage statique et l'exécution des fonctions enregistrées std :: atexit (18.5 ), le programme a un comportement indéfini. [Remarque: S'il y a une utilisation d'un objet avec une durée de stockage statique qui ne se produit pas avant la destruction de l'objet, le programme a un comportement indéfini. Terminer chaque thread avant un appel à std :: exit ou la sortie de main est suffisant, mais pas nécessaire, pour satisfaire ces exigences. Ces exigences autorisent les gestionnaires de threads en tant qu'objets de durée de stockage statique. —Fin note]
5. L'appel de la fonction std :: abort () déclarée dans "cstdlib" termine le programme sans exécuter aucun destructeur et sans appeler les fonctions passées à std :: atexit () ou std :: at_quick_exit ().
Interprétation:
- la destruction d'objets dont la durée de stockage des threads est effectuée dans l'ordre inverse de leur création;
- strictement après cela, les objets avec une durée de stockage statique sont détruits et des appels sont effectués vers des fonctions enregistrées avec std :: atexit dans l'ordre inverse de la création de ces objets et de l'enregistrement de ces fonctions;
- Une tentative d'accès à un objet détruit avec une durée de stockage de thread ou une durée de stockage statique contient un comportement non défini. La réinitialisation de ces objets n'est pas fournie.
Remarque: les variables globales dans la norme sont appelées "variable non locale avec une durée de stockage statique". En conséquence, il s'avère que toutes les variables globales, tous les singletones (statiques locales) et tous les appels à std :: atexit tombent dans une seule file d'attente LIFO lorsqu'ils sont créés / enregistrés.
Les informations utiles pour l'article sont également contenues dans la section
3.6.2 Initialisation des variables non locales [basic.start.init] . J'apporte seulement les plus importants:
L'initialisation dynamique d'une variable non locale avec une durée de stockage statique est ordonnée ou non ordonnée. [...] Les variables à initialisation ordonnée définies au sein d'une même unité de traduction doivent être initialisées dans l'ordre de leurs définitions dans l'unité de traduction.
Interprétation (en tenant compte du texte intégral de la section): les variables globales au sein d'une unité de traduction sont initialisées dans l'ordre de déclaration.
Ce qui sera dans le code
Tous les exemples de code fournis dans l'article sont publiés sur le
github .
Le code se compose de trois couches, comme s'il était écrit par des personnes différentes:
- singleton;
- utilitaire (classe utilisant singleton);
- utilisateur (variables globales et principales).
Singleton et l'utilitaire sont comme une bibliothèque tierce et l'utilisateur est l'utilisateur.
La couche utilitaire est conçue pour isoler la couche utilisateur de la couche singleton. Dans les exemples, l'utilisateur a la possibilité d'accéder au singleton, mais nous agirons comme si c'était impossible.
L'utilisateur fait d'abord tout correctement, puis d'un coup de poignet, tout se casse. Nous essayons d'abord de le corriger dans la couche utilitaire, et si cela ne fonctionne pas, puis dans la couche singleton.
Dans le code, nous marcherons constamment le long du bord - maintenant du côté clair, puis du noir. Pour faciliter le passage du côté obscur, le cas le plus difficile a été choisi: accéder à un singleton à partir du destructeur d'utilitaires.
Pourquoi le cas de l'appel du destructeur est-il le plus difficile? Parce que le destructeur d'utilitaires peut être appelé dans le processus de minimisation de l'application, lorsque la question «le singleton a-t-il été détruit ou pas encore» devient pertinente.
L'affaire est une sorte de synthétique. En pratique, les appels à un singleton depuis le destructeur ne sont pas nécessaires. Même au besoin. Par exemple, pour consigner la destruction d'objets.
Trois classes de singleton sont utilisées:
- SingletonClassic - pas de pointeurs intelligents. En fait, ce n'est pas directement tout à fait classique, mais certainement le plus classique parmi les trois considérés;
- SingletonShared - avec std :: shared_ptr;
- SingletonWeak - avec std :: faible_ptr.
Tous les singletones sont des modèles. Le paramètre de modèle est utilisé pour en hériter. Dans la plupart des exemples, ils sont paramétrés par la classe Payload, qui fournit une fonction publique pour ajouter des données à std :: set.
Dans la plupart des exemples, le destructeur d'utilitaires essaie de remplir une centaine de valeurs. La sortie de diagnostic vers la console est également utilisée à partir du constructeur singleton, du destructeur singleton et de instance ().
Pourquoi si dur? Pour qu'il soit plus facile de comprendre que nous sommes du côté obscur. L'appel au singleton détruit est un comportement indéfini, mais il ne peut se manifester d'aucune façon en externe. Le bourrage de valeurs dans le std :: set détruit ne garantit pas non plus les manifestations externes, mais il n'y a pas de moyen plus fiable (en fait, dans GCC sous Linux dans des exemples incorrects avec le singleton classique, le std :: set détruit est correctement bourré, et dans MSVS sous Windows - se bloque). Avec un comportement non défini, la sortie vers la console peut
ne pas se produire. Ainsi, dans les exemples corrects, nous nous attendons à l'absence d'accès à l'instance () après le destructeur, ainsi qu'à l'absence d'un crash et de l'absence d'un blocage, et dans les mauvais, soit la présence d'un tel appel, ou d'un crash, ou d'un blocage, ou tout d'un coup dans n'importe quelle combinaison, ou quoi que ce soit.
Singleton classique
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; } };
Exemple 1 de SingletonClassic
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; }
Sortie consoleinstance ()
SingletonClassic ()
instance ()
~ SingletonClassic ()
L'utilitaire appelle le singleton dans le constructeur pour s'assurer que le singleton est créé avant la création de l'utilitaire.
L'utilisateur crée deux std :: unique_ptr: un vide, le second contenant l'utilitaire.
L'ordre de création:
- vide std :: unique_ptr.
- singleton;
- utilité.
Et en conséquence, l'ordre de destruction:
- utilité;
- singleton;
- vide std :: unique_ptr.
L'appel du destructeur d'utilitaires au singleton est correct.
Exemple 2 de SingletonClassic
Tout est pareil, mais l'utilisateur l'a pris et a tout gâché avec une seule ligne.
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; }
Sortie consoleinstance ()
SingletonClassic ()
~ SingletonClassic ()
instance ()
L'ordre de création et de destruction est préservé. Il semblerait que tout soit encore. Mais non. En appelant emptyUnique.swap (utilityUnique), l'utilisateur a commis un comportement non défini.
Pourquoi l'utilisateur a-t-il fait des choses aussi stupides? Parce qu'il ne sait rien de la structure interne de la bibliothèque, qui lui a fourni un singleton et une utilité.
Et si vous connaissez la structure interne de la bibliothèque? ... de toute façon, dans le vrai code, il est très facile de s’impliquer. Et vous devez sortir par un débagage douloureux, car comprendre ce qui s'est exactement passé ne sera pas facile.
Pourquoi ne pas exiger que la bibliothèque soit utilisée correctement? Eh bien, il y a toutes sortes de quais à écrire, des exemples ... Et pourquoi ne pas faire une bibliothèque pas si facile à gâcher?
Exemple 3 de SingletonClassic
Au cours de la préparation de l'article pendant plusieurs jours, j'ai cru qu'il était impossible d'éliminer le comportement indéfini de l'exemple précédent dans la couche utilitaire, et la solution n'était disponible que dans la couche singleton. Mais au fil du temps, une solution a néanmoins été trouvée.
Avant d'ouvrir les spoilers avec le code et l'explication, je suggère au lecteur d'essayer de trouver un moyen de sortir de la situation par lui-même (uniquement dans la couche utilitaire!). Je n'exclus pas qu'il existe de meilleures solutions.
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; }
Sortie consoleinstance ()
SingletonClassic ()
instance ()
instance ()
~ SingletonClassic ()
ExplicationLe problème se produit uniquement lors de la réduction de l'application. Un comportement indéfini peut être éliminé en apprenant à l'utilitaire à reconnaître lorsque l'application est minimisée. Pour ce faire, nous avons utilisé une variable flag_strong du type std :: shared_ptr, qui a un qualificatif de durée de stockage des threads (voir des extraits de la norme dans l'article ci-dessus) - c'est comme une statique, mais elle n'est détruite que lorsque le thread actuel se termine avant que toute statique ne soit détruite , y compris avant la destruction singleton. La variable flag_strong est une pour l'ensemble du flux, et chaque instance de l'utilitaire stocke sa copie faible.
Dans un sens étroit, la solution peut être appelée un hack, car elle est indirecte et non évidente. De plus, il avertit trop tôt et parfois (dans une application multithread) avertit généralement faux. Mais au sens large, ce n'est pas un hack, mais une solution complètement définie par les propriétés standard - à la fois des inconvénients et des avantages.
Singletonshared
Passons à un singleton modifié basé sur 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; } };
Ai-ai-ai, le nouvel opérateur ne doit pas être utilisé dans le code moderne, à la place std :: make_shared est nécessaire! Et cela est empêché par le constructeur privé du singleton.
Ha! J'ai aussi un problème! Déclarez std :: make_shared un ami singleton! ... et obtenez une variante de l'anti-modèle PublicMorozov: en utilisant le même std :: make_shared, il sera possible de créer des instances supplémentaires du singleton non fournies par l'architecture.
Exemples 1 et 2 de SingletonShared
Correspond entièrement aux exemples n ° 1 et 2 pour la version classique. Des modifications importantes n'ont été apportées qu'à la couche singleton, l'utilité est restée essentiellement la même. Tout comme dans les exemples avec le singleton classique, l'exemple-1 est correct et l'exemple-2 montre un comportement indéfini.
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; }
Sortie consoleinstance ()
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; }
Sortie consoleinstance ()
SingletonShared ()
~ SingletonShared ()
instance ()
Exemple 3 de SingletonShared
Et maintenant, nous allons essayer de résoudre ce problème mieux que dans l'exemple numéro 3 des classiques.
La solution est évidente: il vous suffit de prolonger la durée de vie du singleton en stockant une copie de std :: shared_ptr retournée par le singleton dans l'utilitaire. Et cette solution, complète avec SingletonShared, a été largement répliquée dans des sources ouvertes.
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; }
Sortie consoleinstance ()
SingletonShared ()
~ SingletonShared ()
Et maintenant, attention, la question est:
vouliez-vous vraiment prolonger la vie d'un singleton?Ou vouliez-vous vous débarrasser d'un comportement indéfini et choisir la prolongation de la vie comme un chemin à la surface?
L'inexactitude théorique sous la forme d'une substitution d'objectifs conduit à un risque de blocage (ou de référence cyclique - appelez-le comme vous voulez).
Oui nuuuuuu, c'est comme ça qu'il faut essayer si fort!? Vous devrez trouver si longtemps, et vous ne le ferez certainement pas par accident!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()
Sortie consoleinstance ()
SingletonShared ()
SharedSingleThreadedUtility ()
SomethingWithVeryImportantDestructor ()
Un singleton a été créé.
Un utilitaire a été créé.
Quelque chose de S-Very-Important-Destructor a été créé (j'ai ajouté cela pour l'intimidation, car sur Internet il y a des messages comme "eh bien, le destructeur singleton ne sera pas appelé, alors quoi de cela, il doit exister tout le temps programmes ").
Mais aucun destructeur n'a été appelé pour aucun de ces objets!
Ă€ cause de quoi? En raison de la substitution des buts par des moyens.
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; } };
Une telle modification du singleton dans les sources ouvertes, si elle est donnée, n'est certainement pas souvent. Je suis tombé sur d'étranges options tournées à l'envers avec un std :: faible_ptr, qui semble être utilisé, qui, semble-t-il, n'offre rien de plus que de prolonger la vie d'un singleton:
L'option que je propose, lorsqu'elle est appliquée correctement dans les couches singleton et utilitaires:
- protège contre les actions dans la couche utilisateur décrite dans les exemples ci-dessus, y compris empêche l'impasse;
- détermine le moment du pliage de l'application plus précisément que l'application thread_local dans Classic_Example3_correct, c'est-à -dire vous permet de vous rapprocher du bord;
- Je ne souffre pas du problème théorique de substitution d'objectifs par des moyens (je ne sais pas si quelque chose de tangible autre que l'impasse peut apparaître de ce problème théorique).
Cependant, il y a un inconvénient: prolonger la durée de vie d'un singleton peut
encore lui permettre
de se
rapprocher encore plus du bord.
Exemple 1 de SingletonWeak
Similaire Ă 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; }
Sortie consoleinstance ()
SingletonWeak ()
~ SingletonWeak ()
Pourquoi avons-nous besoin de SingletonWeak, car personne ne dérange l'utilitaire pour utiliser SingletonShared comme SingletonWeak? Oui, personne ne dérange. Et même personne ne dérange l'utilitaire pour utiliser SingletonWeak comme SingletonShared. Mais les utiliser à leur destination est légèrement plus facile que de les utiliser à d'autres fins.
Exemple 2 de SingletonWeak
Similaire Ă Shared_Example4_incorrect, mais seul un blocage ne se produit pas dans ce cas.
Weak_Example2_correct.cpp #include "SingletonWeak.h" #include "CallbackPayload.h" #include "SomethingWithVeryImportantDestructor.h" class WeakSingleThreadedUtility { public: WeakSingleThreadedUtility()
Sortie consoleinstance ()
SingletonWeak ()
WeakSingleThreadedUtility ()
SomethingWithVeryImportantDestructor ()
~ SingletonWeak ()
~ SomethingWithVeryImportantDestructor ()
~ WeakSingleThreadedUtility ()
Au lieu d'une conclusion
Et quoi, une telle modification d'un singleton éliminera un comportement indéfini? J'ai promis qu'il n'y aurait pas de fin heureuse. Les exemples suivants montrent que des actions de sabotage habiles dans la couche utilisateur peuvent détruire même la bibliothèque réfléchie correcte avec un singleton (mais nous devons admettre que
cela ne peut guère être fait par accident).
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; }
Sortie consoleinstance ()
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; }
Sortie consoleinstance ()
SingletonWeak ()
~ SingletonWeak ()
instance ()