web-dev-qa-db-fra.com

Motif de conception Singleton C ++

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?

667
Artem Barger

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

1003
Martin York

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.

46
Reed Copsey

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.

34
Cătălin Pitiș

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() {}
};
12
Galik

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.

9
James Hopkin

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.

6
SadSido

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);
5
riderchap

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>

4
Yuriy

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.

2
T.E.D.

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.

2
Red.Wave

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).

1
dan-man

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

0
baris.aydinoz