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!
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". –
@Mike - gran referencia, y estoy totalmente de acuerdo. –