Pattern Singleton in C ++

Ho una domanda sul modello singleton.

Ho visto due casi riguardanti il ​​membro statico nella class Singleton.

Innanzitutto è un object, come questo

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

Uno è un puntatore, come questo

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

Qual è la differenza tra i due casi? Quale è corretto?

Probabilmente dovresti leggere il libro di Alexandrescu.

Per quanto riguarda la statica locale, non uso Visual Studio da un po ‘, ma durante la compilazione con Visual Studio 2003, c’era uno statico locale assegnato per DLL … parla di un incubo di debug, mi ricorderò quello per un mentre :/

1. Durata di un Singleton

Il problema principale dei singleton è la gestione a vita.

Se provi a usare l’object, devi essere vivo e vegeto. Il problema deriva quindi sia dall’inizializzazione che dalla distruzione, che è un problema comune in C ++ con i globali.

L’inizializzazione è solitamente la cosa più semplice da correggere. Come suggeriscono entrambi i metodi, è abbastanza semplice inizializzarsi al primo utilizzo.

La distruzione è un po ‘più delicata. le variabili globali vengono distrutte nell’ordine inverso in cui sono state create. Quindi, nel caso statico locale, non controlli effettivamente le cose ….

2. Locale statico

 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; 

Qual è il problema qui? Controlliamo l’ordine in cui sono chiamati costruttori e distruttori.

Innanzitutto, la fase di costruzione:

  • A globalA; viene eseguito, viene chiamato A::A()
  • A::A() chiama B::B()
  • A::A() chiama C::C()

Funziona bene, perché inizializziamo le istanze B e C al primo accesso.

In secondo luogo, la fase di distruzione:

  • C::~C() è chiamato perché era l’ultimo costruito del 3
  • B::~B() è chiamato … oups, tenta di accedere all’istanza di C !

Abbiamo quindi un comportamento indefinito alla distruzione, hum …

3. La nuova strategia

L’idea qui è semplice. i built-in globali sono inizializzati prima degli altri globals, quindi il tuo puntatore sarà impostato su 0 prima che il codice che hai scritto venga richiamato, assicurando che il test:

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

Verificherà effettivamente se l’istanza è corretta o meno.

Tuttavia, è stato detto, c’è una perdita di memoria qui e peggio un distruttore che non viene mai chiamato. La soluzione esiste ed è standardizzata. È una chiamata alla funzione atexit .

La funzione atexit ti consente di specificare un’azione da eseguire durante l’arresto del programma. Con ciò, possiamo scrivere a singleton:

 // 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!!! 

In primo luogo, impariamo di più su atexit . La firma è int atexit(void (*function)(void)); , cioè accetta un puntatore a una funzione che non accetta nulla come argomento e non restituisce nulla.

In secondo luogo, come funziona? Bene, esattamente come il caso d’uso precedente: all’inizializzazione costruisce una pila dei puntatori per funzionare per chiamare e alla distruzione svuota la pila di un object alla volta. Quindi, in effetti, le funzioni vengono chiamate in modalità Last-In First-Out.

Cosa succede qui allora?

  • Costruzione al primo accesso (l’inizializzazione va bene), registro il metodo CleanUp per il tempo di uscita

  • Tempo di uscita: viene chiamato il metodo CleanUp . Distrugge l’object (quindi possiamo efficacemente lavorare nel distruttore) e resettare il puntatore su 0 per segnalarlo.

Cosa succede se (come nell’esempio con A , B e C ) richiamo l’istanza di un object già distrutto? Bene, in questo caso, poiché ho impostato il puntatore su 0 , ricostruirò un singleton temporaneo e il ciclo ricomincia. Non durerà a lungo anche se sto depilando il mio stack.

Alexandrescu la chiamò Phoenix Singleton in quanto risorge dalle sue ceneri se è necessaria dopo che è stata distrutta.

Un’altra alternativa è avere un flag statico e impostarlo su destroyed durante la pulizia e far sapere all’utente che non ha ottenuto un’istanza del singleton, ad esempio restituendo un puntatore nullo. L’unico problema che ho con la restituzione di un puntatore (o riferimento) è che è meglio sperare che nessuno sia abbastanza stupido da chiamare la delete su di esso: /

4. Il modello monoide

Dato che stiamo parlando di Singleton penso che sia ora di introdurre il Pattern Monoid . In sostanza, può essere visto come un caso degenerato del modello Flyweight o un uso di Proxy su Singleton .

Il pattern Monoid è semplice: tutte le istanze della class condividono uno stato comune.

Ne approfitterò per esporre l’implementazione di 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; } 

Qual è il vantaggio? Nasconde il fatto che lo stato è condiviso, nasconde il Singleton .

  • Se hai mai bisogno di avere 2 stati distinti, è ansible che tu riuscirai a farlo senza cambiare ogni riga di codice che lo ha usato (ad esempio, sostituendo Singleton con una chiamata ad una Factory )
  • Nodoby chiamerà delete sull’istanza del tuo singleton, quindi gestisci davvero lo stato e previeni gli incidenti … comunque non puoi fare molto contro gli utenti malintenzionati!
  • Tu controlli l’accesso al singleton, quindi nel caso in cui venga chiamato dopo che è stato distrutto puoi gestirlo correttamente (non fare nulla, accedere, ecc …)

5. Ultima parola

Per quanto completo possa sembrare, vorrei sottolineare che ho felicemente sfogliato qualsiasi problema multithread … leggi il Modern C ++ di Alexandrescu per saperne di più!

Né è più corretto dell’altro. Tenderei a cercare di evitare l’uso di Singleton in generale, ma quando ho dovuto pensare che era la strada da percorrere, ho usato entrambi e hanno funzionato bene.

Un nodo con l’opzione del puntatore è che perde memoria. D’altra parte, il tuo primo esempio potrebbe finire per essere distrutto prima che tu abbia finito, così avrai una battaglia da intraprendere indipendentemente dal fatto che tu non scelga di capire un proprietario più appropriato per questa cosa, che può crea e distruggi al momento giusto.

La differenza è che il secondo perde memoria (il singleton stesso) mentre il primo non lo fa. Gli oggetti statici vengono inizializzati una volta la prima volta che viene chiamato il loro metodo associato e (a condizione che il programma esca pulito) vengano distrutti prima che il programma esca. La versione con il puntatore lascerà il puntatore allocato all’uscita del programma e controller di memoria come Valgrind si lamenteranno.

Inoltre, cosa impedisce a qualcuno di fare delete GlobalClass::instance(); ?

Per i due motivi di cui sopra, la versione che utilizza lo statico è il metodo più comune e quello prescritto nel libro Design Pattern originale.

Usa il secondo approccio: se non vuoi usare atexit per liberare il tuo object, puoi sempre usare l’object custode (ad esempio auto_ptr o qualcosa di auto-scritto). Questo potrebbe causare la liberazione prima che tu abbia finito con l’object, proprio come con il primo metodo.

La differenza è che se si utilizza l’object statico, in pratica non si ha modo di verificare se è già stato liberato o meno.

Se si utilizza il puntatore, è ansible aggiungere un valore bool statico aggiuntivo per indicare se Singleton è già stato distrutto (come in Monoid). Quindi il tuo codice può sempre controllare se Singleton è già stato distrutto e, sebbene tu possa fallire in ciò che intendi fare, almeno non avrai criptico “errore di segmentazione” o “violazione di accesso” e il programma eviterà la chiusura anomala.

Sono d’accordo con Billy. Nel secondo approccio stiamo allocando dynamicmente la memoria dall’heap usando new . Questa memoria rimane sempre e non viene mai liberata, a meno che non sia stata effettuata una chiamata per eliminare . Quindi l’approccio del puntatore globale crea una perdita di memoria.

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

Il tuo primo esempio è più tipico per un singleton. Il secondo esempio differisce dal fatto che è creato su richiesta.

Tuttavia, tenterei di evitare l’uso di singleton in generale poiché non sono altro che variabili globali.

Un approccio migliore consiste nel creare una class singleton. Ciò evita anche il controllo della disponibilità dell’istanza nella funzione GetInstance (). Questo può essere ottenuto usando un puntatore a funzione.

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

In risposta ai reclami relativi alla “perdita di memoria”, esiste una soluzione semplice:

 // dtor ~GlobalClass() { if (this == s_instance) s_instance = NULL; } 

In altre parole, assegnare alla class un distruttore che desinizializza la variabile del puntatore nascosta quando l’object Singleton viene distrutto al momento della terminazione del programma.

Una volta che hai fatto questo, le due forms sono praticamente identiche. L’unica differenza significativa è che si restituisce un riferimento a un object nascosto mentre l’altro restituisce un puntatore ad esso.

Aggiornare

Come sottolinea @BillyONeal, questo non funzionerà perché l’object puntato non viene mai cancellato . Ahia.

Detesto perfino pensarci, ma potresti usare atexit() per fare il lavoro sporco. Sheesh.

Oh, bene, non importa.