Patrón Singleton en C ++

Tengo una pregunta sobre el patrón singleton.

Vi dos casos relacionados con el miembro estático en la clase singleton.

Primero es un objeto, como este

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 es un puntero, como este

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

¿Cuál es la diferencia entre los dos casos? ¿Cuál es correcto?

Probablemente deberías leer el libro de Alexandrescu.

En cuanto a la estática local, no he usado Visual Studio por un tiempo, pero cuando compilé con Visual Studio 2003, había una asignación estática local por DLL … hablamos de una pesadilla de depuración, recordaré esa para una mientras :/

1. Tiempo de vida de un Singleton

El principal problema sobre singletons es la gestión de por vida.

Si alguna vez intentas usar el objeto, debes estar vivo y coleando. El problema proviene de la inicialización y la destrucción, que es un problema común en C ++ con globales.

La inicialización suele ser lo más fácil de corregir. Como sugieren ambos métodos, es lo suficientemente simple como para inicializarse en el primer uso.

La destrucción es un poco más delicada. las variables globales se destruyen en el orden inverso en el que se crearon. Entonces, en el caso estático local, en realidad no controlas las cosas …

2. Local estático

 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; 

¿Cuál es el problema aquí? Vamos a verificar el orden en que se llaman los constructores y los destructores.

Primero, la fase de construcción:

  • A globalA; se ejecuta, se llama A::A()
  • A::A() llama a B::B()
  • A::A() llama a C::C()

Funciona bien, porque inicializamos las instancias B y C en el primer acceso.

En segundo lugar, la fase de destrucción:

  • C::~C() se llama porque fue el último construido de los 3
  • B::~B() se llama … oups, ¡intenta acceder a la instancia de C !

Por lo tanto, tenemos un comportamiento indefinido en la destrucción, hum …

3. La nueva estrategia

La idea aquí es simple. las incorporaciones globales se inicializan antes que las otras variables globales, por lo que su puntero se establecerá en 0 antes de que se llame a cualquiera de los códigos que ha escrito, esto asegura que la prueba:

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

Verificará si la instancia es correcta o no.

Sin embargo, se ha dicho que hay una pérdida de memoria aquí y lo peor es un destructor al que nunca se llama. La solución existe, y está estandarizada. Es una llamada a la función atexit .

La función atexit permite especificar una acción para ejecutar durante el cierre del progtwig. Con eso, podemos escribir un Singleton 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!!! 

Primero, aprendamos más sobre atexit . La firma es int atexit(void (*function)(void)); , es decir, acepta un puntero a una función que no toma nada como argumento y tampoco devuelve nada.

En segundo lugar, ¿cómo funciona? Bueno, exactamente como en el caso de uso anterior: en la inicialización construye una stack de punteros para funcionar para llamar y en la destrucción vacía la stack un elemento a la vez. Por lo tanto, en efecto, las funciones se llaman en la forma Last-In First-Out.

¿Qué pasa aquí entonces?

  • Construcción en el primer acceso (la inicialización es buena), registro el método CleanUp para el tiempo de salida

  • Hora de salida: se llama al método CleanUp . Destruye el objeto (por lo tanto, podemos hacer un trabajo eficaz en el destructor) y restablece el puntero a 0 para señalizarlo.

¿Qué sucede si (como en el ejemplo con A , B y C ) invoco la instancia de un objeto ya destruido? Bueno, en este caso, dado que retrocedí el puntero a 0 , reconstruiré un singleton temporal y el ciclo comenzará de nuevo. No vivirá por mucho tiempo, ya que estoy aprovisionando mi stack.

Alexandrescu lo llamó Phoenix Singleton mientras resucita de sus cenizas si es necesario después de que fue destruido.

Otra alternativa es tener un indicador estático y configurarlo para que se destroyed durante la limpieza y hacerle saber al usuario que no obtuvo una instancia del singleton, por ejemplo devolviendo un puntero nulo. El único problema que tengo al devolver un puntero (o referencia) es que es mejor esperar que nadie sea tan estúpido como para llamar delete en él: /

4. El patrón Monoid

Ya que estamos hablando de Singleton , creo que es hora de introducir el Patrón Monoid . En esencia, se puede ver como un caso degenerado del patrón Flyweight , o un uso de Proxy sobre Singleton .

El patrón Monoid es simple: todas las instancias de la clase comparten un estado común.

Aprovecharé la oportunidad para exponer la implementación no de 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; } 

¿Cuál es el beneficio? Oculta el hecho de que el estado es compartido, oculta el Singleton .

  • Si alguna vez necesita tener 2 estados distintos, es posible que logre hacerlo sin cambiar cada línea de código que lo usó (reemplazando el Singleton por una llamada a una Factory por ejemplo)
  • Nodoby va a llamar a delete en la instancia de tu singleton, por lo que realmente administrar el estado y evitar accidentes … ¡no se puede hacer mucho contra los usuarios maliciosos de todos modos!
  • Usted controla el acceso al singleton, por lo que en caso de que se llame después de haber sido destruido, puede manejarlo correctamente (no hacer nada, iniciar sesión, etc.)

5. Última palabra

Tan completo como parezca, me gustaría señalar que felizmente he analizado cualquier problema de múltiples hilos … ¡lea C ++ Modern de Alexandrescu para obtener más información!

Ninguno es más correcto que el otro. Tiende a tratar de evitar el uso de Singleton en general, pero cuando he tenido que pensar que era el camino a seguir, he usado ambos y han funcionado bien.

Un enganche con la opción de puntero es que perderá memoria. Por otro lado, su primer ejemplo puede terminar siendo destruido antes de que termine con él, por lo que tendrá que librar una batalla independientemente de si no elige encontrar un propietario más apropiado para esta cosa, que puede crear y destruir en el momento adecuado.

La diferencia es que el segundo pierde memoria (el singleton mismo) mientras que el primero no. Los objetos estáticos se inicializan una vez que se llama a su método asociado y (siempre que el progtwig salga limpiamente) se destruyen antes de que el progtwig finalice. La versión con el puntero dejará el puntero asignado al salir del progtwig y las comprobaciones de memoria como Valgrind se quejarán.

Además, lo que impide que alguien lo haga delete GlobalClass::instance(); ?

Por las dos razones anteriores, la versión que usa la estática es el método más común y el que se prescribe en el libro original Patrones de diseño.

Utilice el segundo enfoque: si no quiere usar atexit para liberar su objeto, entonces siempre puede usar el objeto keeper (por ejemplo, auto_ptr, o algo escrito por uno mismo). Esto podría causar la liberación antes de que haya terminado con el objeto, al igual que con el primer primer método.

La diferencia es que si usas un objeto estático, básicamente no tienes forma de comprobar si ya se liberó o no.

Si usa el puntero, puede agregar bool estático adicional para indicar si el singleton ya fue destruido (como en Monoid). Entonces su código siempre puede verificar si el singleton ya fue destruido, y aunque puede fallar en lo que pretende hacer, al menos no obtendrá crípticas “fallas de segmentación” o “violaciones de acceso”, y el progtwig evitará una terminación anormal.

Estoy de acuerdo con Billy. En el segundo enfoque estamos asignando dinámicamente la memoria desde el montón utilizando new . Esta memoria permanece siempre y nunca se libera, a menos que se realice una llamada para eliminar . Por lo tanto, el enfoque del puntero Global crea una pérdida de 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; } 

Su primer ejemplo es más típico para un singleton. Su segundo ejemplo difiere en que se crea bajo demanda.

Sin embargo, trataría de evitar el uso de singleton en general ya que no son más que variables globales.

Un mejor enfoque es crear una clase singleton. Esto también evita la verificación de disponibilidad de instancias en la función GetInstance (). Esto se puede lograr usando un puntero de función.

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

En respuesta a las quejas de “fuga de memoria”, existe una solución fácil:

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

En otras palabras, otorgue a la clase un destructor que anule la inicialización de la variable del puntero oculto cuando el objeto singleton se destruye al momento de la finalización del progtwig.

Una vez que hayas hecho esto, las dos formas son prácticamente idénticas. La única diferencia significativa es que uno devuelve referencias a un objeto oculto mientras que el otro le devuelve un puntero.

Actualizar

Como señala @BillyONeal, esto no funcionará porque el objeto apuntado nunca se elimina . Ay.

Odio siquiera pensar en ello, pero podrías usar atexit() para hacer el trabajo sucio. Sheesh.

Oh bien, ya no importa.