46 votes

Modèle singleton en C++

J'ai une question sur le modèle singleton.

J'ai vu deux cas concernant le membre statique dans la classe singleton.

Il s'agit d'abord d'un objet, comme ceci

class CMySingleton
{
public:
  static CMySingleton& Instance()
  {
    static CMySingleton singleton;
    return singleton;
  }

// Other non-static member functions
private:
  CMySingleton() {}                                  // Private constructor
  ~CMySingleton() {}
  CMySingleton(const CMySingleton&);                 // Prevent copy-construction
  CMySingleton& operator=(const CMySingleton&);      // Prevent assignment
};

L'un est un pointeur, comme ceci

class GlobalClass
{
    int m_value;
    static GlobalClass *s_instance;
    GlobalClass(int v = 0)
    {
        m_value = v;
    }
  public:
    int get_value()
    {
        return m_value;
    }
    void set_value(int v)
    {
        m_value = v;
    }
    static GlobalClass *instance()
    {
        if (!s_instance)
          s_instance = new GlobalClass;
        return s_instance;
    }
};

Quelle est la différence entre les deux cas ? Lequel est correct ?

60voto

Matthieu M. Points 101624

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 !

4voto

dash-tom-bang Points 9384

Aucun n'est plus correct que l'autre. J'aurais tendance à essayer d'éviter l'utilisation de Singleton en général, mais lorsque j'ai été confronté au fait de penser que c'était la voie à suivre, j'ai utilisé les deux et ils ont bien fonctionné.

Un problème avec l'option du pointeur est qu'il y a une fuite de mémoire. D'un autre côté, votre premier exemple peut finir par être détruit avant que vous n'en ayez fini avec lui, donc vous aurez une bataille à mener si vous ne choisissez pas de trouver un propriétaire plus approprié pour cette chose, qui peut la créer et la détruire aux bons moments.

2voto

Billy ONeal Points 50631

La différence réside dans le fait que le second fait fuir la mémoire (le singleton lui-même) alors que le premier ne le fait pas. Les objets statiques sont initialisés une fois la première fois que leur méthode associée est appelée, et (tant que le programme se termine proprement) ils sont détruits avant la fin du programme. La version avec le pointeur laissera le pointeur alloué à la sortie du programme et les vérificateurs de mémoire comme Valgrind se plaindront.

Aussi, qu'est-ce qui empêche quelqu'un de faire delete GlobalClass::instance(); ?

Pour les deux raisons ci-dessus, la version utilisant le statique est la méthode la plus courante et celle prescrite dans le livre original Design Patterns.

1voto

j_kubik Points 4007

Utilisez la deuxième approche - si vous ne voulez pas utiliser atexit pour libérer votre objet, alors vous pouvez toujours utiliser keeper object (eg. auto_ptr, ou quelque chose d'écrit par vous-même). Cela peut entraîner la libération de l'objet avant que vous en ayez fini avec lui, tout comme avec la première méthode.

La différence est que si vous utilisez un objet statique, vous n'avez aucun moyen de vérifier s'il a déjà été libéré ou non.

Si vous utilisez un pointeur, vous pouvez ajouter un bool statique supplémentaire pour indiquer si le singleton a déjà été détruit (comme dans Monoid). Votre code pourra alors toujours vérifier si le singleton a déjà été détruit, et bien que vous puissiez échouer dans ce que vous avez l'intention de faire, au moins vous n'obtiendrez pas le cryptique "segmentaion fault" ou "access violation", et le programme évitera une terminaison anormale.

1voto

Sagnik Points 11

Je suis d'accord avec Billy. Dans la deuxième approche, nous allouons dynamiquement de la mémoire à partir du tas en utilisant la fonction nouveau . Cette mémoire reste en permanence et n'est jamais libérée, sauf si un appel à la fonction supprimer a été réalisée. L'approche par pointeur global crée donc une fuite de mémoire.

class singleton
{
    private:
        static singleton* single;
        singleton()
        {  }
        singleton(const singleton& obj)
        {  }

    public:
        static singleton* getInstance();
        ~singleton()
        {
            if(single != NULL)
            {
                single = NULL;
            }
        }
};

singleton* singleton :: single=NULL;
singleton* singleton :: getInstance()
{
    if(single == NULL)
    {
        single = new singleton;
    }
    return single;
}

int main() {
    singleton *ptrobj = singleton::getInstance();
    delete ptrobj;

    singleton::getInstance();
    delete singleton::getInstance();
    return 0;
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X