Tu devrais probablement lire le livre d'Alexandrescu.
En ce qui concerne les statiques locales, je n'ai pas utilisé Visual Studio depuis un certain temps, mais lors de la compilation avec Visual Studio 2003, il y avait une statique locale allouée par DLL... c'était un cauchemar pour le débogage, je m'en souviendrai longtemps :/.
1. Durée de vie d'un singleton
Le principal problème des singletons est la gestion de leur durée de vie.
Si vous essayez d'utiliser l'objet, vous devez être en vie et en pleine forme. Le problème vient donc à la fois de l'initialisation et de la destruction, ce qui est un problème courant en C++ avec les globaux.
L'initialisation est généralement la chose la plus facile à corriger. Comme les deux méthodes le suggèrent, il est assez simple d'initialiser à la première utilisation.
La destruction est un peu plus délicate. Les variables globales sont détruites dans l'ordre inverse de leur création. Ainsi, dans le cas d'une variable locale statique, vous ne contrôlez pas vraiment les choses.....
2. Statique locale
struct A
{
A() { B::Instance(); C::Instance().call(); }
};
struct B
{
~B() { C::Instance().call(); }
static B& Instance() { static B MI; return MI; }
};
struct C
{
static C& Instance() { static C MI; return MI; }
void call() {}
};
A globalA;
Quel est le problème ici ? Vérifions l'ordre dans lequel les constructeurs et les destructeurs sont appelés.
Tout d'abord, la phase de construction :
-
A globalA;
est exécuté, A::A()
s'appelle
-
A::A()
appelle B::B()
-
A::A()
appelle C::C()
Cela fonctionne bien, car nous initialisons B
y C
instances lors du premier accès.
Deuxièmement, la phase de destruction :
-
C::~C()
est appelé parce qu'il était le dernier construit des 3
-
B::~B()
est appelé... oups, il tente d'accéder à C
de l'instance !
Nous avons donc un comportement indéfini à la destruction, hum...
3. La nouvelle stratégie
L'idée ici est simple. Les build-ins globaux sont initialisés avant les autres globaux, donc votre pointeur sera mis à 0
avant qu'une partie du code que vous avez écrit ne soit appelée, cela garantit que le test :
S& S::Instance() { if (MInstance == 0) MInstance = new S(); return *MInstance; }
Vérifie effectivement si l'instance est correcte ou non.
Cependant, cela a été dit, il y a une fuite de mémoire ici et au pire un destructeur qui n'est jamais appelé. La solution existe, et elle est normalisée. Il s'agit d'un appel au atexit
fonction.
El atexit
vous permettent de spécifier une action à exécuter lors de l'arrêt du programme. Avec cela, nous pouvons écrire un singleton alright :
// in s.hpp
class S
{
public:
static S& Instance(); // already defined
private:
static void CleanUp();
S(); // later, because that's where the work takes place
~S() { /* anything ? */ }
// not copyable
S(S const&);
S& operator=(S const&);
static S* MInstance;
};
// in s.cpp
S* S::MInstance = 0;
S::S() { atexit(&CleanUp); }
S::CleanUp() { delete MInstance; MInstance = 0; } // Note the = 0 bit!!!
Tout d'abord, nous allons en savoir plus sur atexit
. La signature est int atexit(void (*function)(void));
c'est-à-dire qu'il accepte un pointeur vers une fonction qui ne prend rien comme argument et ne renvoie rien non plus.
Deuxièmement, comment cela fonctionne-t-il ? Eh bien, exactement comme le cas d'utilisation précédent : à l'initialisation, il construit une pile de pointeurs vers les fonctions à appeler et à la destruction, il vide la pile un élément à la fois. Donc, en fait, les fonctions sont appelées selon le principe du dernier entré, premier sorti.
Que se passe-t-il alors ici ?
-
Construction lors du premier accès (l'initialisation se passe bien), j'enregistre la CleanUp
méthode pour le temps de sortie
-
Temps de sortie : le CleanUp
est appelée. Elle détruit l'objet (ainsi nous pouvons effectivement faire du travail dans le destructeur) et réinitialise le pointeur à 0
pour le signaler.
Que se passe-t-il si (comme dans l'exemple avec A
, B
y C
) Je fais appel à l'instance d'un objet déjà détruit ? Eh bien, dans ce cas, puisque j'ai remis le pointeur sur l'objet 0
Je reconstruis un singleton temporaire et le cycle recommence. Il ne vivra pas longtemps cependant puisque je dépile ma pile.
Alexandrescu a appelé le Phoenix Singleton
car il ressuscite de ses cendres s'il en a besoin après avoir été détruit.
Une autre alternative est d'avoir un drapeau statique et de le définir à destroyed
pendant le nettoyage et faire savoir à l'utilisateur qu'il n'a pas obtenu une instance du singleton, par exemple en renvoyant un pointeur nul. Le seul problème que j'ai avec le retour d'un pointeur (ou d'une référence) est que vous feriez mieux d'espérer que personne ne soit assez stupide pour appeler delete
sur elle :/
4. Le modèle monoïde
Puisque nous parlons de Singleton
Je pense qu'il est temps de présenter le Monoid
Un motif. En substance, il peut être considéré comme un cas dégénéré de la Flyweight
ou une utilisation de Proxy
sur Singleton
.
El Monoid
est simple : toutes les instances de la classe partagent un état commun.
Je profite de l'occasion pour exposer l'implémentation non-Phoenix :)
class Monoid
{
public:
void foo() { if (State* i = Instance()) i->foo(); }
void bar() { if (State* i = Instance()) i->bar(); }
private:
struct State {};
static State* Instance();
static void CleanUp();
static bool MDestroyed;
static State* MInstance;
};
// .cpp
bool Monoid::MDestroyed = false;
State* Monoid::MInstance = 0;
State* Monoid::Instance()
{
if (!MDestroyed && !MInstance)
{
MInstance = new State();
atexit(&CleanUp);
}
return MInstance;
}
void Monoid::CleanUp()
{
delete MInstance;
MInstance = 0;
MDestroyed = true;
}
Quel est l'avantage ? Cela cache le fait que l'état est partagé, cela cache le Singleton
.
- Si vous avez un jour besoin d'avoir 2 états distincts, il est possible que vous réussissiez à le faire sans changer chaque ligne de code qui l'utilisait (en remplaçant l'attribut
Singleton
par un appel à un Factory
par exemple)
- Nodoby va appeler
delete
sur l'instance de votre singleton, afin de vraiment gérer l'état et d'éviter les accidents... vous ne pouvez pas faire grand chose contre les utilisateurs malveillants de toute façon !
- Vous contrôlez l'accès au singleton, de sorte que si celui-ci est appelé après avoir été détruit, vous pouvez le gérer correctement (ne rien faire, enregistrer, etc...).
5. Dernier mot
Aussi complet que cela puisse paraître, je tiens à préciser que j'ai volontiers survolé les questions relatives au multithread... lisez Modern C++ d'Alexandrescu pour en savoir plus !