Modello di progettazione Singleton C ++


736

Di recente mi sono imbattuto in una realizzazione / implementazione del modello di progettazione Singleton per C ++. È stato così (l'ho adottato dall'esempio della vita reale):

// a lot of methods are omitted here
class Singleton
{
   public:
       static Singleton* getInstance( );
       ~Singleton( );
   private:
       Singleton( );
       static Singleton* instance;
};

Da questa dichiarazione posso dedurre che il campo dell'istanza è iniziato sull'heap. Ciò significa che c'è un'allocazione di memoria. Ciò che non è completamente chiaro per me è quando esattamente la memoria verrà deallocata? O c'è un bug e una perdita di memoria? Sembra che ci sia un problema nell'implementazione.

La mia domanda principale è: come posso implementarla nel modo giusto?



10
In questo documento troverai una grande discussione su come implementare un singleton, insieme alla sicurezza dei thread in C ++. aristeia.com/Papers/DDJ%5FJul%5FAug%5F2004%5Frevised.pdf

106
@sbi - Solo un Sith si occupa di assoluti. La stragrande maggioranza dei problemi può essere risolta senza Singleton? Assolutamente. Singletons causa problemi per conto proprio? Sì. Tuttavia, non posso onestamente dire che sono cattivi , dal momento che il design consiste nel considerare i compromessi e comprendere le sfumature del tuo approccio.
Derekerdmann,

11
@derekerdmann: non ho detto che non hai mai bisogno di una variabile globale (e quando ne hai bisogno, a volte Singleton è meglio). Quello che ho detto è che dovrebbero essere usati il ​​meno possibile. Glorificare Singleton come un prezioso modello di design dà l'impressione che sia bello usarlo, piuttosto che essere un hack , rendendo il codice difficile da capire, difficile da mantenere e difficile da testare. Questo è il motivo per cui ho pubblicato il mio commento. Nulla di ciò che hai detto finora lo ha contraddetto.
sabato

13
@sbi: Quello che hai detto è "Non usarli". Non tanto più ragionevole "dovrebbero essere usati il ​​meno possibile" in seguito, sicuramente vedrai la differenza.
jwd,

Risposte:


1106

Nel 2008 ho fornito un'implementazione in C ++ 98 del modello di progettazione Singleton che è lazy-rated, garanzia di distruzione, non tecnicamente sicura per i thread:
qualcuno può fornirmi un campione di Singleton in c ++?

Ecco un'implementazione C ++ 11 aggiornata del modello di progettazione Singleton valutato in modo pigro, distrutto correttamente e sicuro per i thread .

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

Vedi questo articolo su quando usare un singleton: (non spesso)
Singleton: come dovrebbe essere usato

Consulta questo articolo sull'ordine di inizializzazione e su come gestirlo:
ordine di inizializzazione delle variabili statiche
Individuazione dei problemi relativi all'ordine di inizializzazione statica C ++

Vedi questo articolo che descrive i tempi di vita:
qual è la durata di una variabile statica in una funzione C ++?

Vedi questo articolo che discute alcune implicazioni del threading sui singleton:
istanza Singleton dichiarata come variabile statica del metodo GetInstance, è thread-safe?

Leggi questo articolo che spiega perché il doppio controllo bloccato non funzionerà su C ++:
quali sono tutti i comportamenti comuni non definiti che un programmatore C ++ dovrebbe conoscere?
Dr Dobbs: C ++ e The Perils of Double-Checked Locking: Part I


23
Buona risposta. Ma dovrebbe notare che questo non è thread-safe stackoverflow.com/questions/1661529/...
Varuna

4
@zourtney: Molte persone non si rendono conto di ciò che hai appena fatto :)
Johann Gerell il

4
@MaximYegorushkin: quando questo viene distrutto è molto ben definito (non c'è ambiguità). Vedi: stackoverflow.com/questions/246564/…
Martin York,

3
What irks me most though is the run-time check of the hidden boolean in getInstance()Questo è un presupposto sulla tecnica di implementazione. Non è necessario presupporre che sia vivo. consultare stackoverflow.com/a/335746/14065 È possibile forzare una situazione in modo che sia sempre viva (meno di quanto sopra Schwarz counter). Le variabili globali hanno più problemi con l'ordine di inizializzazione (tra le unità di compilazione) poiché non si forza un ordine. Il vantaggio di questo modello è 1) inizializzazione pigra. 2) Capacità di far rispettare un ordine (Schwarz aiuta ma è più brutto). Sì, get_instance()è molto più brutto.
Martin York,

3
@kol: No. Non è il solito. Solo perché i principianti copiano e incollano il codice senza pensare non lo rendono il solito. Dovresti sempre esaminare il caso d'uso e assicurarti che l'operatore di assegnazione faccia ciò che è previsto. Copia e incolla il codice ti porterà ad errori.
Martin York,

47

Essendo un Singleton, di solito non vuoi che venga distrutto.

Verrà abbattuto e deallocato al termine del programma, che è il comportamento normale desiderato per un singleton. Se vuoi essere in grado di ripulirlo esplicitamente, è abbastanza facile aggiungere un metodo statico alla classe che ti consenta di ripristinarlo in uno stato pulito e di riallocarlo la prossima volta che viene utilizzato, ma non rientra nell'ambito di un singleton "classico".


4
se il comando delete non viene mai chiamato esplicitamente nell'istanza statica Singleton *, questo non sarebbe tecnicamente considerato una perdita di memoria?
Andrew Garrison,

7
Non è più una perdita di memoria di una semplice dichiarazione di una variabile globale.
ilya n.

15
Per chiarire qualcosa ... le "perdite di memoria" nei confronti dei singoli sono completamente irrilevanti. Se disponi di risorse statali in cui l'ordine di decostruzione conta, i singoli possono essere pericolosi; ma tutta la memoria viene recuperata in modo pulito dal sistema operativo al termine del programma ... annullando questo punto totalmente accademico nel 99,9% dei casi. Se vuoi discutere la grammatica avanti e indietro di ciò che è e non è una "perdita di memoria", va bene, ma renditi conto che è una distrazione dalle reali decisioni di progettazione.
jkerian,

12
@jkerian: Perdite di memoria e distruzione nel contesto C ++ non riguardano in realtà la perdita di memoria. In realtà si tratta di controllo delle risorse. Se si perde memoria, il distruttore non viene chiamato e quindi tutte le risorse associate all'oggetto non vengono rilasciate correttamente. La memoria è solo il semplice esempio che usiamo per insegnare la programmazione ma ci sono risorse molto più complesse là fuori.
Martin York,

7
@Martin Sono completamente d'accordo con te. Anche se l'unica risorsa è la memoria, avrai comunque problemi a cercare perdite REALI nel tuo programma se devi sfogliare un elenco di perdite, filtrando quelle che "non contano". È meglio ripulirli tutti, quindi qualsiasi strumento che segnala perdite segnala solo cose che SONO un problema.
Dolphin,

38

È possibile evitare l'allocazione di memoria. Esistono molte varianti, tutte con problemi in caso di ambiente multithreading.

Preferisco questo tipo di implementazione (in realtà, non si dice correttamente che preferisco, perché evito il più possibile i singoli punti):

class Singleton
{
private:
   Singleton();

public:
   static Singleton& instance()
   {
      static Singleton INSTANCE;
      return INSTANCE;
   }
};

Non ha allocazione dinamica della memoria.


3
In alcuni casi, questa inizializzazione pigra non è il modello ideale da seguire. Un esempio è se il costruttore del singleton alloca memoria dall'heap e si desidera che tale allocazione sia prevedibile, ad esempio in un sistema incorporato o in un altro ambiente strettamente controllato. Preferisco, quando il modello Singleton è il modello migliore da usare, per creare l'istanza come membro statico della classe.
dma,

3
Per molti programmi più grandi, specialmente quelli con librerie dinamiche. Qualsiasi oggetto globale o statico che non sia primitivo può portare a segfault / crash all'uscita del programma su molte piattaforme a causa di problemi di distruzione durante lo scarico delle librerie. Questo è uno dei motivi per cui molte convenzioni di codifica (incluso Google) vietano l'uso di oggetti statici e globali non banali.
obecalp,

Sembra che l'istanza statica in tale implementazione abbia un collegamento interno e avrà copie uniche e indipendenti in diverse unità di traduzione, il che causerà comportamenti confusi e sbagliati. Ma ho visto molte di queste implementazioni, mi sto perdendo qualcosa?
FaceBro,

Cosa impedisce all'utente di assegnare questo a più oggetti in cui il compilatore dietro le quinte utilizza il proprio costruttore di copie?
Tony Tannous,

19

La risposta di @Loki Astari è eccellente.

Tuttavia, ci sono momenti con più oggetti statici in cui è necessario essere in grado di garantire che il singleton non verrà distrutto fino a quando tutti gli oggetti statici che usano il singleton non ne avranno più bisogno.

In questo caso std::shared_ptrpuò essere utilizzato per mantenere in vita il singleton per tutti gli utenti anche quando i distruttori statici vengono chiamati alla fine del programma:

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

9

Un'altra alternativa non allocante: crea un singleton, diciamo di classe C, quando ne hai bisogno:

singleton<C>()

utilizzando

template <class X>
X& singleton()
{
    static X x;
    return x;
}

Né questa né la risposta di Cătălin sono automaticamente thread-safe nell'attuale C ++, ma saranno in C ++ 0x.


Attualmente sotto gcc è thread-safe (ed è stato per un po ').
Martin York,

13
Il problema con questo design è che se usato su più librerie. Ogni libreria ha una propria copia del singleton che quella libreria utilizza. Quindi non è più un singleton.
Martin York,

6

Non ho trovato un'implementazione CRTP tra le risposte, quindi eccola:

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

Per usare basta ereditare la tua classe da questo, come: class Test : public Singleton<Test>


1
Non è stato possibile farlo funzionare con C ++ 17 fino a quando non ho protetto il costruttore predefinito e '= default;'.
WFranczyk,

6

La soluzione nella risposta accettata presenta un significativo svantaggio: il distruttore per il singleton viene chiamato dopo che il controllo lascia la main()funzione. Potrebbero esserci davvero dei problemi quando alcuni oggetti dipendenti sono allocati all'internomain .

Ho riscontrato questo problema durante il tentativo di introdurre un Singleton nell'applicazione Qt. Ho deciso che tutte le mie finestre di dialogo di installazione devono essere Singleton e ho adottato il modello sopra. Sfortunatamente, la classe principale di Qt è QApplicationstata allocata nello stack nella mainfunzione e Qt proibisce la creazione / distruzione di finestre di dialogo quando non è disponibile alcun oggetto applicazione.

Questo è il motivo per cui preferisco i singleton allocati in heap. Fornisco un esplicito init()e term()metodi per tutti i singoli e li chiamo all'interno main. Quindi ho il pieno controllo sull'ordine di creazione / distruzione dei singoli, e garantisco anche che verranno creati i singoli, indipendentemente dal fatto che qualcuno abbia chiamato getInstance()o meno.


2
Se ti riferisci alla risposta attualmente accettata, la prima affermazione è errata. Il distruttore non viene chiamato fino alla distruzione di tutti gli oggetti con durata della memoria statica.
Martin York,

5

Ecco una facile implementazione.

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

Viene creato solo un oggetto e questo riferimento all'oggetto viene restituito ogni volta dopo le parole.

SingletonClass instance created!
00915CB8
00915CB8

Qui 00915CB8 è la posizione di memoria dell'oggetto Singleton, uguale per la durata del programma ma (normalmente!) Diverso ogni volta che il programma viene eseguito.

NB Questo non è un filo sicuro. Devi garantire la sicurezza del filo.


5

Se si desidera allocare l'oggetto nell'heap, perché non utilizzare un puntatore univoco. Anche la memoria verrà deallocata poiché stiamo utilizzando un puntatore univoco.

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


2
Questo non è thread-safe. Meglio fare m_sun locale staticdi getInstance()e inizializzare immediatamente senza una prova.
Galik,

2

È probabilmente probabilmente allocato dall'heap, ma senza le fonti non c'è modo di saperlo.

L'implementazione tipica (presa da un codice che ho già in emacs) sarebbe:

Singleton * Singleton::getInstance() {
    if (!instance) {
        instance = new Singleton();
    };
    return instance;
};

... e fare affidamento sul fatto che il programma esca dal campo di applicazione per ripulire in seguito.

Se lavori su una piattaforma in cui la pulizia deve essere eseguita manualmente, probabilmente aggiungerei una routine di pulizia manuale.

Un altro problema nel farlo in questo modo è che non è thread-safe. In un ambiente multithread, due thread potrebbero superare il "if" prima che uno dei due abbia la possibilità di allocare la nuova istanza (quindi entrambi lo farebbero). Questo non è ancora un grosso problema se si fa affidamento sulla conclusione del programma per ripulire comunque.


puoi dedurlo, poiché puoi vedere che la variabile di istanza è un puntatore all'istanza di classe.
Artem Barger,

3
Non è necessario allocare dinamicamente il singleton. In realtà questa è una cattiva idea in quanto non c'è modo di disallocare automaticamente usando il disegno sopra. Lasciarlo uscire dal campo di applicazione non si chiama distruttori ed è solo pigro.
Martin York,

Puoi deallocare automaticamente usando la funzione atexit. Questo è quello che facciamo (senza dire che è una buona idea)
Joe,

2

Qualcuno ha menzionato std::call_oncee std::once_flag? La maggior parte degli altri approcci, incluso il blocco con doppio controllo, non funziona.

Un grave problema nell'implementazione del modello singleton è l'inizializzazione sicura. L'unico modo sicuro è proteggere la sequenza di inizializzazione con barriere di sincronizzazione. Ma queste stesse barriere devono essere avviate in sicurezza. std::once_flagè il meccanismo per ottenere un'inizializzazione sicura garantita.


2

Di recente abbiamo approfondito questo argomento nella mia classe EECS. Se vuoi guardare le note della lezione in dettaglio, visita http://umich.edu/~eecs381/lecture/IdiomsDesPattsCreational.pdf

Ci sono due modi che conosco per creare correttamente una classe Singleton.

Primo modo:

Implementalo in modo simile al modo in cui lo hai nel tuo esempio. Per quanto riguarda la distruzione, "I singoli di solito durano per la durata dell'esecuzione del programma; la maggior parte dei sistemi operativi recupererà la memoria e la maggior parte delle altre risorse quando un programma termina, quindi c'è un argomento per non preoccuparsi di questo".

Tuttavia, è buona norma ripulire al termine del programma. Pertanto, è possibile farlo con una classe SingletonDestructor ausiliaria statica e dichiararlo come amico nel proprio Singleton.

class Singleton {
public:
  static Singleton* get_instance();

  // disable copy/move -- this is a Singleton
  Singleton(const Singleton&) = delete;
  Singleton(Singleton&&) = delete;
  Singleton& operator=(const Singleton&) = delete;
  Singleton& operator=(Singleton&&) = delete;

  friend class Singleton_destroyer;

private:
  Singleton();  // no one else can create one
  ~Singleton(); // prevent accidental deletion

  static Singleton* ptr;
};

// auxiliary static object for destroying the memory of Singleton
class Singleton_destroyer {
public:
  ~Singleton_destroyer { delete Singleton::ptr; }
};

Il Singleton_destroyer verrà creato all'avvio del programma e "quando il programma termina, tutti gli oggetti globali / statici vengono distrutti dal codice di arresto della libreria di runtime (inserito dal linker), quindi il_destroyer verrà distrutto; il suo distruttore eliminerà il Singleton, eseguendo il suo distruttore."

Secondo modo

Questo si chiama Meyers Singleton, creato dal mago del C ++ Scott Meyers. Definisci semplicemente get_instance () in modo diverso. Ora puoi anche sbarazzarti della variabile membro puntatore.

// public member function
static Singleton& Singleton::get_instance()
{
  static Singleton s;
  return s;
}

Questo è pulito perché il valore restituito è per riferimento ed è possibile utilizzare la .sintassi invece di ->accedere alle variabili dei membri.

"Il compilatore crea automaticamente il codice che crea la prima volta attraverso la dichiarazione, non successivamente, e quindi elimina l'oggetto statico al termine del programma."

Si noti inoltre che con Meyers Singleton è possibile "trovarsi in una situazione molto difficile se gli oggetti si basano l'uno sull'altro al momento della terminazione - quando Singleton scompare rispetto ad altri oggetti? Ma per applicazioni semplici, funziona bene".


1

Oltre all'altra discussione qui, può valere la pena notare che si può avere la globalità, senza limitare l'utilizzo a un'istanza. Ad esempio, considera il caso di riferimento contando qualcosa ...

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

Ora da qualche parte all'interno di una funzione (come main) puoi fare:

auto ref1_a = ItemRef<&store1_g>(101);
auto ref2_a = ItemRef<&store2_g>(201); 

Non è necessario che i ref memorizzino un puntatore nei rispettivi Storeperché tali informazioni vengono fornite in fase di compilazione. Inoltre, non devi preoccuparti della Storevita perché il compilatore richiede che sia globale. Se c'è davvero solo un'istanza di Storeallora non ci sono spese generali in questo approccio; con più di un'istanza spetta al compilatore essere intelligente sulla generazione del codice. Se necessario, la ItemRefclasse può anche essere composta friendda Store(puoi avere degli amici templati!).

Se di per Storesé è una classe basata su modelli, le cose diventano più difficili, ma è ancora possibile utilizzare questo metodo, forse implementando una classe di supporto con la seguente firma:

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'utente può ora creare un StoreWrappertipo (e un'istanza globale) per ogni Storeistanza globale e accedere sempre ai negozi tramite la loro istanza wrapper (dimenticando così i dettagli cruenti dei parametri del modello necessari per l'utilizzo Store).


0

Si tratta della gestione della durata degli oggetti. Supponiamo di avere più di singoli nel tuo software. E dipendono da Logger singleton. Durante la distruzione dell'applicazione, supponiamo che un altro oggetto singleton utilizzi Logger per registrare i suoi passaggi di distruzione. Devi garantire che Logger debba essere pulito per ultimo. Pertanto, consulta anche questo documento: http://www.cs.wustl.edu/~schmidt/PDF/ObjMan.pdf


0

La mia implementazione è simile a quella di Galik. La differenza è che la mia implementazione consente ai puntatori condivisi di ripulire la memoria allocata, invece di trattenerla fino a quando l'applicazione non viene chiusa e i puntatori statici vengono ripuliti.

#pragma once

#include <memory>

template<typename T>
class Singleton
{
private:
  static std::weak_ptr<T> _singleton;
public:
  static std::shared_ptr<T> singleton()
  {
    std::shared_ptr<T> singleton = _singleton.lock();
    if (!singleton) 
    {
      singleton.reset(new T());
      _singleton = singleton;
    }

    return singleton;
  }
};

template<typename T>
std::weak_ptr<T> Singleton<T>::_singleton;

0

Il codice è corretto, tranne per il fatto che non hai dichiarato il puntatore all'istanza al di fuori della classe . Le dichiarazioni di variabili statiche all'interno della classe non sono considerate dichiarazioni in C ++, tuttavia ciò è consentito in altri linguaggi come C # o Java ecc.

class Singleton
{
   public:
       static Singleton* getInstance( );
   private:
       Singleton( );
       static Singleton* instance;
};
Singleton* Singleton::instance; //we need to declare outside because static variables are global

Devi sapere che l'istanza Singleton non deve essere eliminata manualmente da noi . Abbiamo bisogno di un singolo oggetto per tutto il programma, quindi alla fine dell'esecuzione del programma, verrà automaticamente deallocato.


-1

Il documento che è stato collegato sopra descrive il difetto del doppio controllo verificato è che il compilatore può allocare la memoria per l'oggetto e impostare un puntatore all'indirizzo della memoria allocata, prima che il costruttore dell'oggetto sia stato chiamato. È abbastanza facile in c ++ usare gli allocatori per allocare manualmente la memoria e quindi usare una chiamata di costruzione per inizializzare la memoria. Usando questa valutazione, la chiusura a doppio controllo funziona bene.


2
Sfortunatamente no. Questo è stato discusso in modo approfondito da alcuni dei migliori sviluppatori C ++ in circolazione. Il blocco con doppio controllo è interrotto in C ++ 03.
Martin York,

-1
#define INS(c) private:void operator=(c const&){};public:static c& I(){static c _instance;return _instance;}

Esempio:

   class CCtrl
    {
    private:
        CCtrl(void);
        virtual ~CCtrl(void);

    public:
        INS(CCtrl);

-1

Semplice classe singleton, deve essere il file della classe di intestazione

#ifndef SC_SINGLETON_CLASS_H
#define SC_SINGLETON_CLASS_H

class SingletonClass
{
    public:
        static SingletonClass* Instance()
        {
           static SingletonClass* instance = new SingletonClass();
           return instance;
        }

        void Relocate(int X, int Y, int Z);

    private:
        SingletonClass();
        ~SingletonClass();
};

#define sSingletonClass SingletonClass::Instance()

#endif

Accedi al tuo singleton in questo modo:

sSingletonClass->Relocate(1, 2, 5);

-3

Penso che dovresti scrivere una funzione statica in cui il tuo oggetto statico viene eliminato. Dovresti chiamare questa funzione quando stai per chiudere la tua applicazione. Ciò assicurerà di non avere perdite di memoria.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.