web-dev-qa-db-fra.com

Motif Singleton en C++

J'ai une question sur le motif singleton.

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

D'abord c'est un objet, comme ça

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
};

On est un pointeur, comme ça

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? Laquelle est correcte?

46
skydoor

Vous devriez probablement lire le livre d'Alexandrescu.

En ce qui concerne la statique locale, je n’ai pas utilisé Visual Studio depuis un moment, mais lors de la compilation avec Visual Studio 2003, il y avait une statique locale allouée par DLL ... parler d’un cauchemar de débogage, je me souviendrai de celui-ci pour un tandis que :/

1. La vie d'un singleton

Le problème principal concernant les singletons est la gestion de la durée de vie.

Si vous essayez d'utiliser l'objet, vous devez être vivant et donner des coups de pied. Le problème provient donc à la fois de l'initialisation et de la destruction, problème courant en C++ avec les globals.

L'initialisation est généralement la chose la plus facile à corriger. Comme le suggèrent les deux méthodes, 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. Donc, dans le cas statique local, vous ne contrôlez pas réellement 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? Voyons dans quel ordre les constructeurs et les destructeurs sont appelés.

Tout d'abord, la phase de construction:

  • A globalA; est exécuté, A::A() est appelé
  • A::A() appelle B::B()
  • A::A() appelle C::C()

Cela fonctionne bien, car nous initialisons les instances B et C lors du premier accès.

Deuxièmement, la phase de destruction:

  • C::~C() est appelé car il s’agissait du dernier construit du 3
  • B::~B() s'appelle ... oups, il tente d'accéder à l'instance de C!

Nous avons donc un comportement indéfini à la destruction, hum ...

3. La nouvelle stratégie

L'idée ici est simple. les éléments intégrés globaux sont initialisés avant les autres éléments globaux; votre pointeur sera donc défini sur 0 avant que le code que vous avez écrit soit appelé, cela garantit que le test:

S& S::Instance() { if (MInstance == 0) MInstance = new S(); return *MInstance; }

Va effectivement vérifier si l'instance est correcte ou non.

Cependant, on a dit, il y a une fuite de mémoire ici et pire un destructeur qui ne se fait jamais appeler. La solution existe et est standardisée. C'est un appel à la fonction atexit.

La fonction atexit vous permet de spécifier une action à exécuter lors de l’arrêt du programme. Avec ça, on peut écrire un singleton, très bien:

// 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, apprenons-en davantage sur atexit. La signature est int atexit(void (*function)(void));, c'est-à-dire qu'elle accepte un pointeur sur une fonction qui ne prend rien en argument et ne retourne rien non plus.

Deuxièmement, comment ça marche? Eh bien, exactement comme dans le cas d'utilisation précédent: lors de l'initialisation, il crée une pile de pointeurs à utiliser pour appeler et, lors de la destruction, vide la pile, un élément à la fois. Ainsi, en réalité, les fonctions sont appelées de la manière utilisée du dernier entré, premier sorti.

Qu'est-ce qui se passe ici alors?

  • Construction lors du premier accès (l’initialisation convient), j’enregistre la méthode CleanUp pour le temps de sortie

  • Heure de sortie: la méthode CleanUp est appelée. Il détruit l'objet (nous pouvons donc travailler efficacement dans le destructeur) et réinitialise le pointeur sur 0 pour le signaler.

Que se passe-t-il si (comme dans l'exemple avec A, B et C) j'appelle sur l'instance d'un objet déjà détruit? Eh bien, dans ce cas, puisque je mets le pointeur sur 0, je reconstruis un singleton temporaire et le cycle recommence. Cela ne va pas durer longtemps depuis que je dépile ma pile.

Alexandrescu l'a appelé le Phoenix Singleton car il ressuscite de ses cendres s'il le faut après sa destruction.

Une autre solution consiste à définir un indicateur statique et à le définir sur destroyed lors du nettoyage et à informer l'utilisateur qu'il n'a pas reçu d'instance du singleton, par exemple en renvoyant un pointeur null. Le seul problème que je rencontre avec le renvoi d'un pointeur (ou d'une référence) est qu'il est préférable d'espérer que personne ne soit assez stupide pour appeler delete: /

4. Le motif monoïde

Puisque nous parlons de Singleton, je pense qu'il est temps d'introduire le modèle Monoid. Essentiellement, cela peut être considéré comme un cas dégénéré du modèle Flyweight ou une utilisation de Proxy sur Singleton.

Le modèle Monoid est simple: toutes les instances de la classe partagent un état commun.

Je vais en profiter pour exposer l'implémentation de not-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? Il cache le fait que l'état est partagé, il cache la Singleton.

  • Si vous avez besoin d'avoir deux états distincts, il est possible que vous le fassiez sans changer chaque ligne de code qui l'a utilisé (en remplaçant la Singleton par un appel à une Factory par exemple)
  • Nodoby va appeler delete sur l'instance de votre singleton, vous gérez ainsi l'état et évitez les accidents ... vous ne pouvez rien faire contre les utilisateurs malveillants de toute façon!
  • Vous contrôlez l'accès au singleton. Ainsi, s'il est appelé après sa destruction, vous pouvez le gérer correctement (ne rien faire, consigner, etc.).

5. Dernier mot

Aussi complet que cela puisse paraître, je voudrais préciser que j’ai heureusement feuilleté tous les problèmes multithreads ... lisez le C++ moderne d’Alexandrescu pour en savoir plus!

60
Matthieu M.

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

Un problème avec l'option de pointeur est qu'il va perdre de la mémoire. D'autre part, votre premier exemple peut finir par être détruit avant que vous n'en ayez fini, vous aurez donc une bataille à mener, peu importe si vous ne choisissez pas un propriétaire plus approprié pour cette chose créez-le et détruisez-le au bon moment.

4
dash-tom-bang

La différence est que le second perd de la mémoire (le singleton lui-même) alors que le premier n'en perd pas. Les objets statiques sont initialisés lors du premier appel de la méthode associée et sont détruits avant la fermeture du programme (tant que le programme se termine correctement). 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 paramètre statique est la méthode la plus courante et celle recommandée dans le manuel Design Patterns d'origine.

2
Billy ONeal

Utilisez la deuxième approche - si vous ne voulez pas utiliser atexit pour libérer votre objet, vous pouvez toujours utiliser un objet gardien (par exemple, auto_ptr ou quelque chose que vous écrivez vous-même). Cela pourrait causer la libération avant que vous n'ayez fini avec object, comme avec la première méthode. 

La différence est que si vous utilisez un objet statique, vous n'avez en gros 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). Ensuite, votre code peut toujours vérifier si le singleton a déjà été détruit, et bien que vous puissiez échouer à ce que vous avez l'intention de faire, au moins vous n'obtiendrez pas une "erreur de segmentation" ou une "violation d'accès" cryptée et le programme évitera une terminaison anormale.

1
j_kubik

Je suis d'accord avec Billy. Dans la deuxième approche, nous allouons dynamiquement la mémoire du tas en utilisant new . Cette mémoire reste toujours et ne sera jamais libérée, sauf si un appel à delete a été effectué. Par conséquent, l'approche du pointeur global crée 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;
}
1
Sagnik

Une meilleure approche consiste à créer une classe singleton. Cela évite également le contrôle de disponibilité d'instance dans la fonction GetInstance (). Ceci peut être réalisé en utilisant un pointeur de fonction.

class TSingleton;

typedef TSingleton* (*FuncPtr) (void);

class TSingleton {

TSingleton(); //prevent public object creation
TSingleton  (const TSingleton& pObject); // prevent copying object
static TSingleton* vObject; // single object of a class

static TSingleton* CreateInstance   (void);
static TSingleton* Instance     (void);
public:

static FuncPtr  GetInstance; 
};


FuncPtr TSingleton::GetInstance = CreateInstance;
TSingleton* TSingleton::vObject;

TSingleton::TSingleton()
{
}

TSingleton::TSingleton(const TSingleton& pObject)
{
}

TSingleton* TSingleton::CreateInstance(void)
{
if(vObject == NULL){

    // Introduce here some code for taking lock for thread safe creation
    //...
    //...
    //...

    if(vObject == NULL){

        vObject = new TSingleton();
        GetInstance = Instance;
    }
}

return vObject;
}

TSingleton* TSingleton::Instance(void)
{

return vObject;

}

void main()
{

TSingleton::GetInstance(); // this will call TSingleton::Createinstance()

TSingleton::GetInstance(); // this will call TSingleton::Instance()

// all further calls to TSingleton::GetInstance will call TSingleton::Instance() which simply returns already created object. 

}
0
Genie

Votre premier exemple est plus typique d'un singleton. Votre deuxième exemple diffère en ce sens qu'il est créé à la demande.

Cependant, j'essaierais d'éviter d'utiliser des singletons en général, car ils ne sont que des variables globales.

0
Skeets