2010-03-23 30 views
46

Tengo una pregunta sobre el patrón singleton.Patrón singleton en C++

Vi dos casos relativos al miembro estático en la clase singleton.

En primer lugar, 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 de ellos 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?

+14

Si usted está realmente interesado, "Modern C++ Diseño" de Alexandrescu tiene un capítulo entero dedicado a tratar de hacer únicos segura y correcta, la exploración de muchos rincones oscuros de la lengua. En mi opinión, se puede resumir como "simplemente no". –

+0

@Mike - gran referencia, y estoy totalmente de acuerdo. –

Respuesta

60

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 estática local asignada por DLL ... hablo de una pesadilla de depuración, lo recordaré que uno por un tiempo:/

1. curso de la vida de un Singleton

la cuestión principal acerca únicos es la gestión de toda la 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. Así, en el caso estática local, que no controlan realmente las cosas ....

2. Local estática

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.

En primer lugar, la fase de construcción:

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

Funciona bien, porque inicializamos B y C instancias en abetos t acceso.

En segundo lugar, la fase de destrucción:

  • C::~C() se llama porque era la última construida de la 3
  • B::~B() se llama ... oups, ella accede a C 's ejemplo!

Así pues, tenemos un comportamiento indefinido a la destrucción, hum ...

3. La nueva estrategia

La idea aquí es simple.muebles empotrados globales se inicializan antes de las otras variables globales, por lo que el puntero se establece en 0 antes de cualquier parte del código que ha escrito se llamará, se asegura que la prueba:

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

realmente comprobar si no la instancia es correcta

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

La función atexit le permite especificar una acción para ejecutar durante el apagado del programa. Con esto, podemos escribir una bien 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!!! 

En primer lugar, vamos a aprender 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 pila de punteros para funcionar para llamar y en la destrucción vacía la pila un elemento a la vez. Por lo tanto, en efecto, las funciones se llaman en la forma Last-In First-Out.

¿Qué sucede aquí entonces?

  • construcción en el primer acceso (inicialización está muy bien), que registra el método CleanUp de tiempo de salida

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

¿Qué pasa si (como en el ejemplo con A, B y C) Pido a la instancia de un objeto ya destruido? Bueno, en este caso, como 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ó el Phoenix Singleton ya que resucita de sus cenizas si es necesario después de que fue destruido.

Otra alternativa es tener un indicador estático y configurarlo en 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 con devolviendo un puntero (o de referencia) es que será mejor que la esperanza de tan estúpida como para llamar delete a él nadie:/

4. El patrón Monoid

Dado que estamos hablando de Singleton Creo que es hora de presentar 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.

Voy a tomar la oportunidad para exponer la aplicación no-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 tiene que tener 2 estados distintos, es posible que usted consigue hacerlo sin cambiar cada línea de código que se utilizó (en sustitución del Singleton por una llamada a un Factory por ejemplo)
  • Nodoby va a llamar al delete en la instancia de su singleton, por lo que realmente administra el estado y evita accidentes ... ¡de todos modos no puede hacer mucho contra los usuarios malintencionados!
  • permite controlar el acceso al producto único, por lo que en caso de que se llama después de haber sido destruida se puede manejar correctamente (no hacer nada, registro, etc ...)

5. Última palabra

Tan completo como parezca, me gustaría señalar que con mucho gusto he analizado cualquier problema de varios hilos ... ¡lea C++ Modern Courses de Alexandrescu para obtener más información!

+3

"El único problema que tengo con devolviendo un puntero (o de referencia) es que le vale que nadie es tan estúpido como para llamar a borrar en ella: /" que haya hecho su destructor privado, por lo que tendrían que ir bien fuera de su camino para hacerlo. –

+0

Monoid - idea interesante. Me gusta. También le permitiría contar de nuevo el singleton interno si lo desea, de modo que solo exista si se está utilizando. – Skeets

+0

@Dennis: correcto, se olvidó de eso. –

4

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.

+3

La memoria filtrada == no es correcta. –

+4

Ok, ¿entonces? Nunca intenté decir que la pérdida de memoria era correcta. Los singletons son peores que las pérdidas de memoria, IMO. –

+0

@ dash-tom-bang: OK. Entendí mal. Me quité mi voto negativo. –

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 singletons en general ya que no son más que variables globales.

+2

El primer ejemplo se crea a demanda también. La estática del método no se crea hasta que se llama a su método por primera vez. –

+0

* nada más que variables globales * ... que a veces necesita. Además, está envuelto agradable y ordenado dentro de su propia clase/espacio de nombres. –

2

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 programa salga limpiamente) se destruyen antes de que el programa finalice. La versión con el puntero dejará el puntero asignado al salir del programa y las comprobaciones de memoria como Valgrind se quejarán.

Además, ¿qué impide que alguien 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 prescrito en el libro de patrones de diseño original.

+0

¿Tiene una cita de lo que es más común? –

+0

@ dash-tom-bang: Sí, el libro original de Patrones de diseño. –

+0

"método más común". Pensé que podría hacer referencia a otra cosa, ya que mi experiencia muestra que las personas usan ambas cosas a tasas uniformes (y la mayoría de las veces las usa incorrectamente, independientemente de la estrategia de implementación). –

-1

En respuesta a las quejas "pérdida de memoria", no es una solución fácil:

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

En otras palabras, dar a la clase un destructor que el DE-inicializa la variable puntero oculta cuando se destruye el objeto singleton al momento de la finalización del programa.

Una vez que haya 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.

actualización

Como @BillyONeal señala, esto no funcionará porque el objeto punta-a nunca se eliminarán . Ay.

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

Oh, bueno, no importa.

+1

No, no se llamará al destructor porque la instancia es un puntero. El destructor no se llama hasta que ese puntero sea 'delete' d. –

+0

Aaagh, tienes razón. Indiferencia. –

1

Utilice la segunda aproximación: si no desea usar atexit para liberar su objeto, entonces siempre puede usar el objeto guardián (por ejemplo, auto_ptr, o algo escrito por sí 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 manera de comprobar si ya se liberó o no.

Si utiliza el puntero, puede agregar bool estático adicional para indicar si el singleton ya se destruyó (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 programa evitará una terminación anormal.

1

Estoy de acuerdo con Billy. En el segundo enfoque estamos asignando memoria dinámicamente desde el montón usando new. Esta memoria permanece siempre y nunca se libera, a menos que se haya realizado una llamada a 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; 
} 
0

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. 

} 
Cuestiones relacionadas