Récemment, je suis tombé sur une réalisation/implémentation du modèle de conception Singleton pour C++. Cela a ressemblé à ceci (je l'ai repris de l'exemple réel):
// a lot of methods are omitted here
class Singleton
{
public:
static Singleton* getInstance( );
~Singleton( );
private:
Singleton( );
static Singleton* instance;
};
De cette déclaration, je peux déduire que le champ d'instance est lancé sur le tas. Cela signifie qu'il y a une allocation de mémoire. Ce qui n’est pas clair pour moi, c’est quand exactement la mémoire va être libérée? Ou y a-t-il un bug et une fuite de mémoire? Il semble qu'il y ait un problème dans la mise en œuvre.
Ma question principale est la suivante: comment puis-je le mettre en œuvre correctement?
En 2008, j'ai fourni une implémentation C++ 98 du modèle de conception Singleton qui est évalué paresseux, avec destruction garantie, sans sécurité technique:
Quelqu'un peut-il me fournir un échantillon de Singleton en c ++?
Voici une implémentation C++ 11 mise à jour du modèle de conception Singleton qui est évaluée paresseuse, détruite correctement et thread-safe .
class S
{
public:
static S& getInstance()
{
static S instance; // Guaranteed to be destroyed.
// Instantiated on first use.
return instance;
}
private:
S() {} // Constructor? (the {} brackets) are needed here.
// C++ 03
// ========
// Don't forget to declare these two. You want to make sure they
// are unacceptable otherwise you may accidentally get copies of
// your singleton appearing.
S(S const&); // Don't Implement
void operator=(S const&); // Don't implement
// C++ 11
// =======
// We can use the better technique of deleting the methods
// we don't want.
public:
S(S const&) = delete;
void operator=(S const&) = delete;
// Note: Scott Meyers mentions in his Effective Modern
// C++ book, that deleted functions should generally
// be public as it results in better error messages
// due to the compilers behavior to check accessibility
// before deleted status
};
Voir cet article pour savoir quand utiliser un singleton: (pas souvent)
Singleton: Comment faut-il l'utiliser
Voir cet article sur l’ordre d’initialisation et la marche à suivre:
Ordre d'initialisation des variables statiques
Recherche des problèmes d'ordre d'initialisation statiques C++
Voir cet article décrivant les durées de vie:
Quelle est la durée de vie d'une variable statique dans une fonction C++?
Voir cet article qui traite de certaines implications de threading pour les singletons:
instance Singleton déclarée comme variable statique de la méthode GetInstance, est-elle thread-safe?
Voir cet article qui explique pourquoi le double verrouillage ne fonctionne pas en C++:
Quels sont tous les comportements non définis communs qu'un programmeur C++ devrait connaître?
Dr Dobbs: C++ et les dangers du verrouillage à double contrôle: partie I
En tant que Singleton, vous ne voulez généralement pas qu’il soit détruit.
Il sera démoli et désalloué à la fin du programme, ce qui est le comportement normal souhaité pour un singleton. Si vous voulez pouvoir le nettoyer explicitement, il est assez facile d'ajouter une méthode statique à la classe qui vous permette de la restaurer dans un état vide et de la réaffecter à sa prochaine utilisation, mais cela sort du cadre d'une application. "classique" singleton.
Vous pourriez éviter l'allocation de mémoire. Il existe de nombreuses variantes, qui rencontrent toutes des problèmes en cas d’environnement multithreading.
Je préfère ce genre d'implémentation (en fait, ce n'est pas dit correctement que je préfère, parce que j'évite autant que possible les singletons):
class Singleton
{
private:
Singleton();
public:
static Singleton& instance()
{
static Singleton INSTANCE;
return INSTANCE;
}
};
Il n'a pas d'allocation de mémoire dynamique.
La réponse de @ Loki Astari est excellente.
Cependant, il existe des cas avec plusieurs objets statiques dans lesquels vous devez pouvoir garantir que le singleton ne sera pas détruit avant que tous vos objets statiques utilisant le singleton n'en a plus besoin.
Dans ce cas, std::shared_ptr
peut être utilisé pour maintenir le singleton actif pour tous les utilisateurs, même lorsque les destructeurs statiques sont appelés à la fin du processus. programme:
class Singleton
{
public:
Singleton(Singleton const&) = delete;
Singleton& operator=(Singleton const&) = delete;
static std::shared_ptr<Singleton> instance()
{
static std::shared_ptr<Singleton> s{new Singleton};
return s;
}
private:
Singleton() {}
};
Une autre alternative non-allocative: créez un singleton, disons de la classe C
, selon vos besoins:
singleton<C>()
en utilisant
template <class X>
X& singleton()
{
static X x;
return x;
}
Ni ceci ni la réponse de Cătălin n'est automatiquement thread-safe en C++ actuel, mais le sera en C++ 0x.
La solution dans la réponse acceptée présente un inconvénient important: le destructeur du singleton est appelé après que le contrôle a quitté la fonction main()
. Il peut effectivement y avoir des problèmes lorsque certains objets dépendants sont alloués à l'intérieur de main
.
J'ai rencontré ce problème en essayant d'introduire un Singleton dans l'application Qt. J'ai décidé que tous mes dialogues d'installation devaient être des singletons et j'ai adopté le modèle ci-dessus. Malheureusement, la classe principale QApplication
de Qt a été allouée sur la pile dans la fonction main
, et Qt interdit la création/destruction de dialogues lorsqu'aucun objet d'application n'est disponible.
C'est pourquoi je préfère les singletons alloués au tas. Je fournis des méthodes explicites init()
et term()
pour tous les singletons et les appelle à l'intérieur de main
. Ainsi, j'ai un contrôle total sur l'ordre de création/destruction de singletons, et je garantis également que des singletons seront créés, que quelqu'un appelle getInstance()
ou non.
Si vous voulez allouer l'objet dans le tas, pourquoi ne pas utiliser un pointeur unique. La mémoire sera également désallouée car nous utilisons un pointeur unique.
class S
{
public:
static S& getInstance()
{
if( m_s.get() == 0 )
{
m_s.reset( new S() );
}
return *m_s;
}
private:
static std::unique_ptr<S> m_s;
S();
S(S const&); // Don't Implement
void operator=(S const&); // Don't implement
};
std::unique_ptr<S> S::m_s(0);
Je n'ai pas trouvé d'implémentation de CRTP parmi les réponses, alors la voici:
template<typename HeirT>
class Singleton
{
public:
Singleton() = delete;
Singleton(const Singleton &) = delete;
Singleton &operator=(const Singleton &) = delete;
static HeirT &instance()
{
static HeirT instance;
return instance;
}
};
Pour utiliser, héritez simplement de votre classe, comme ceci: class Test : public Singleton<Test>
Voici une implémentation facile.
#include <Windows.h>
#include <iostream>
using namespace std;
class SingletonClass {
public:
static SingletonClass* getInstance() {
return (!m_instanceSingleton) ?
m_instanceSingleton = new SingletonClass :
m_instanceSingleton;
}
private:
// private constructor and destructor
SingletonClass() { cout << "SingletonClass instance created!\n"; }
~SingletonClass() {}
// private copy constructor and assignment operator
SingletonClass(const SingletonClass&);
SingletonClass& operator=(const SingletonClass&);
static SingletonClass *m_instanceSingleton;
};
SingletonClass* SingletonClass::m_instanceSingleton = nullptr;
int main(int argc, const char * argv[]) {
SingletonClass *singleton;
singleton = singleton->getInstance();
cout << singleton << endl;
// Another object gets the reference of the first object!
SingletonClass *anotherSingleton;
anotherSingleton = anotherSingleton->getInstance();
cout << anotherSingleton << endl;
Sleep(5000);
return 0;
}
Un seul objet créé et cette référence d'objet est renvoyé chaque fois après.
SingletonClass instance created!
00915CB8
00915CB8
Ici 00915CB8 est l’emplacement mémoire de l’objet singleton, identique pour la durée du programme mais (normalement!) Différent à chaque exécution du programme.
N.B. Ce n'est pas un thread-safe. Vous devez assurer la sécurité des threads.
Il est en effet probablement attribué à partir du tas, mais sans les sources, il n’ya aucun moyen de le savoir.
L’implémentation typique (tirée d’un code que j’ai déjà dans emacs) serait:
Singleton * Singleton::getInstance() {
if (!instance) {
instance = new Singleton();
};
return instance;
};
... et comptez sur le programme hors d'usage pour nettoyer par la suite.
Si vous travaillez sur une plate-forme où le nettoyage doit être effectué manuellement, j'ajouterais probablement une routine de nettoyage manuel.
Un autre problème avec cette méthode est qu’elle n’est pas thread-safe. Dans un environnement multithread, deux threads peuvent passer par le "si" avant que l'un ou l'autre n'ait la possibilité d'allouer la nouvelle instance (les deux le feraient). Ce n’est pas encore un gros problème si vous comptez sur l’arrêt du programme pour nettoyer de toute façon.
Quelqu'un a-t-il mentionné std::call_once
et std::once_flag
? La plupart des autres approches, y compris le verrouillage à double vérification, sont interrompues.
L’initialisation en toute sécurité est un problème majeur dans l’implémentation d’un modèle unique. Le seul moyen sûr est de protéger la séquence d'initialisation avec des barrières de synchronisation. Mais ces obstacles eux-mêmes doivent être initiés en toute sécurité. std::once_flag
est le mécanisme permettant d'obtenir une initialisation sécurisée garantie.
En plus de l’autre discussion ici, il peut être intéressant de noter que vous pouvez avoir une globalité, sans limiter l’utilisation à une seule instance. Par exemple, considérons le cas d'une référence comptant quelque chose ...
struct Store{
std::array<Something, 1024> data;
size_t get(size_t idx){ /* ... */ }
void incr_ref(size_t idx){ /* ... */}
void decr_ref(size_t idx){ /* ... */}
};
template<Store* store_p>
struct ItemRef{
size_t idx;
auto get(){ return store_p->get(idx); };
ItemRef() { store_p->incr_ref(idx); };
~ItemRef() { store_p->decr_ref(idx); };
};
Store store1_g;
Store store2_g; // we don't restrict the number of global Store instances
Maintenant quelque part dans une fonction (telle que main
), vous pouvez faire:
auto ref1_a = ItemRef<&store1_g>(101);
auto ref2_a = ItemRef<&store2_g>(201);
Les référents n'ont pas besoin de stocker un pointeur sur leur Store
respective, car cette information est fournie au moment de la compilation. De plus, vous n'avez pas à vous soucier de la vie de Store
car le compilateur exige qu'elle soit globale. S'il n'y a en effet qu'une seule instance de Store
, il n'y a pas de surcharge dans cette approche; avec plus d'une instance, il appartient au compilateur d'être intelligent en matière de génération de code. Si nécessaire, la classe ItemRef
peut même devenir une friend
sur Store
(vous pouvez avoir des amis modèles!).
Si Store
elle-même est une classe basée sur un modèle, les choses se compliquent, mais il est toujours possible d'utiliser cette méthode, peut-être en implémentant une classe d'assistance avec la signature suivante:
template <typename Store_t, Store_t* store_p>
struct StoreWrapper{ /* stuff to access store_p, e.g. methods returning
instances of ItemRef<Store_t, store_p>. */ };
L'utilisateur peut maintenant créer un type StoreWrapper
(et une instance globale) pour chaque instance globale Store
, et toujours accéder aux magasins via leur instance d'encapsuleur (en oubliant ainsi les détails sanglants des paramètres de modèle nécessaires à l'utilisation Store
).
Il s’agit de la gestion de la durée de vie des objets. Supposons que votre logiciel contient plus que des singletons. Et ils dépendent de singger Logger. Lors de la destruction de l'application, supposons qu'un autre objet singleton utilise Logger pour consigner ses étapes de destruction. Vous devez garantir que Logger sera nettoyé en dernier. Par conséquent, veuillez également consulter ce document: http://www.cs.wustl.edu/~schmidt/PDF/ObjMan.pdf