9.4. Singleton

Posiblemente, el patrón de diseño más simple del GoF es el Singleton, que es una forma de asegurar una única instancia de una clase. El siguiente programa muestra cómo implementar un Singleton en C++:

//: C10:SingletonPattern.cpp
#include <iostream>
using namespace std;

class Singleton {
  static Singleton s;
  int i;
  Singleton(int x) : i(x) { }
  Singleton& operator=(Singleton&);  // Disallowed
  Singleton(const Singleton&);       // Disallowed
public:
  static Singleton& instance() { return s; }
  int getValue() { return i; }
  void setValue(int x) { i = x; }
};

Singleton Singleton::s(47);

int main() {
  Singleton& s = Singleton::instance();
  cout << s.getValue() << endl;
  Singleton& s2 = Singleton::instance();
  s2.setValue(9);
  cout << s.getValue() << endl;
} ///:~

Listado 9.3. C10/SingletonPattern.cpp


La clave para crear un Singleton es evitar que el programador cliente tenga control sobre el ciclo de vida del objeto. Para lograrlo, declare todos los constructores privados, y evite que el compilador genere implícitamente cualquier constructor. Fíjese que el FIXME: constructor de copia? y el operador de asignación (que intencionadamente carecen de implementación alguna, ya que nunca van a ser llamados) están declarados como privados, para evitar que se haga cualquier tipo de copia.

También debe decidir cómo va a crear el objeto. Aquí, se crea de forma estática, pero también puede esperar a que el programador cliente pida uno y crearlo bajo demanda. Esto se llama "inicialización vaga", y sólo tiene sentido si resulta caro crear el objeto y no siempre se necesita.

Si devuelve un puntero en lugar de una referencia, el usuario podría borrar el puntero sin darse cuenta, por lo que la implementación citada anteriormente es más segura (el destructor también podría declararse privado o protegido para solventar el problema). En cualquier caso, el objeto debería almacenarse de forma privada.

Usted da acceso a través de FIXME (funciones de miembros) públicas. Aquí, instance() genera una referencia al objeto Singleton. El resto de la interfaz (getValue() y setValue()) es la interfaz regular de la clase.

Fíjese en que no está restringido a crear un único objeto. Esta técnica también soporta la creacion de un pool limitado de objetos. En este caso, sin embargo, puede enfrentarse al problema de compartir objetos del pool. Si esto supone un problema, puede crear una solución que incluya un check-out y un check-in de los objetos compartidos.

9.4.1. Variantes del Singleton

Cualquier miembro static dentro de una clase es una forma de Singleton se hará uno y sólo uno. En cierto modo, el lenguaje da soporte directo a esta idea; se usa de forma regular. Sin embargo, los objetos estáticos tienen un problema (ya miembros o no): el orden de inicialización, tal y como se describe en el volumen 1 de este libro. Si un objeto static depende de otro, es importante que los objetos se inicializen en el orden correcto.

En el volumen 1, se mostró cómo controlar el orden de inicialización definiendo un objeto estático dentro de una función. Esto retrasa la inicialización del objeto hasta la primera vez que se llama a la función. Si la función devuelve una referencia al objeto estático, hace las veces de Singleton a la vez que elimina gran parte de la preocupación de la inicialización estática. Por ejemplo, suponga que quiere crear un fichero de log en la primera llamada a una función que devuelve una referencia a dicho fichero. Basta con este fichero de cabecera:

//: C10:LogFile.h
#ifndef LOGFILE_H
#define LOGFILE_H
#include <fstream>
std::ofstream& logfile();
#endif // LOGFILE_H ///:~

Listado 9.4. C10/LogFile.h


La implementación no debe FIXME: hacerse en la misma línea, porque eso significaría que la función entera, incluída la definición del objeto estático que contiene, podría ser duplicada en cualquier unidad de traducción donde se incluya, lo que viola la regla de única definición de C++. [137] Con toda seguridad, esto frustraría cualquier intento de controlar el orden de inicialización (pero potencialmente de una forma sutil y difícil de detectar). De forma que la implementación debe separarse:

//: C10:LogFile.cpp {O}
#include "LogFile.h"
std::ofstream& logfile() {
  static std::ofstream log("Logfile.log");
  return log;
} ///:~

Listado 9.5. C10/LogFile.cpp


Ahora el objeto log no se inicializará hasta la primera vez que se llame a logfile(). Así que, si crea una función:

//: C10:UseLog1.h
#ifndef USELOG1_H
#define USELOG1_H
void f();
#endif // USELOG1_H ///:~

Listado 9.6. C10/UseLog1.h


que use logfile() en su implementación:

//: C10:UseLog1.cpp {O}
#include "UseLog1.h"
#include "LogFile.h"
void f() {
  logfile() << __FILE__ << std::endl;
} ///:~

Listado 9.7. C10/UseLog1.cpp


y utiliza logfile() otra vez en otro fichero:

//: C10:UseLog2.cpp
//{L} LogFile UseLog1
#include "UseLog1.h"
#include "LogFile.h"
using namespace std;
void g() {
  logfile() << __FILE__ << endl;
}

int main() {
  f();
  g();
} ///:~

Listado 9.8. C10/UseLog2.cpp


el objecto log no se crea hasta la primera llamada a f().

Puede combinar fácilmente la creación de objetos estáticos dentro de una función miembro con la clase Singleton. SingletonPattern.cpp puede modificarse para usar esta aproximación:[138]

//: C10:SingletonPattern2.cpp
// Meyers' Singleton.
#include <iostream>
using namespace std;

class Singleton {
  int i;
  Singleton(int x) : i(x) { }
  void operator=(Singleton&);
  Singleton(const Singleton&);
public:
  static Singleton& instance() {
    static Singleton s(47);
    return s;
  }
  int getValue() { return i; }
  void setValue(int x) { i = x; }
};

int main() {
  Singleton& s = Singleton::instance();
  cout << s.getValue() << endl;
  Singleton& s2 = Singleton::instance();
  s2.setValue(9);
  cout << s.getValue() << endl;
} ///:~

Listado 9.9. C10/SingletonPattern2.cpp


Se da un caso especialmente interesante cuando dos Singletons dependen mutuamente el uno del otro, de esta forma:

//: C10:FunctionStaticSingleton.cpp

class Singleton1 {
  Singleton1() {}
public:
  static Singleton1& ref() {
    static Singleton1 single;
    return single;
  }
};

class Singleton2 {
  Singleton1& s1;
  Singleton2(Singleton1& s) : s1(s) {}
public:
  static Singleton2& ref() {
    static Singleton2 single(Singleton1::ref());
    return single;
  }
  Singleton1& f() { return s1; }
};

int main() {
  Singleton1& s1 = Singleton2::ref().f();
} ///:~

Listado 9.10. C10/FunctionStaticSingleton.cpp


Cuando se llama a Singleton2::ref(), hace que se cree su único objeto Singleton2. En el proceso de esta creación, se llama a Singleton1::ref(), y esto hace que se cree su objeto único Singleton1. Como esta técnica no se basa en el orden de linkado ni el de carga, el programador tiene mucho mayor control sobre la inicialización, lo que redunda en menos problemas.

Otra variación del Singleton separa la unicidad de un objeto de su implementación. Esto se logra usando el "Patrón Plantilla Curiosamente Recursivo" mencionado en el Capítulo 5:

//: C10:CuriousSingleton.cpp
// Separates a class from its Singleton-ness (almost).
#include <iostream>
using namespace std;

template<class T> class Singleton {
  Singleton(const Singleton&);
  Singleton& operator=(const Singleton&);
protected:
  Singleton() {}
  virtual ~Singleton() {}
public:
  static T& instance() {
    static T theInstance;
    return theInstance;
  }
};

// A sample class to be made into a Singleton
class MyClass : public Singleton<MyClass> {
  int x;
protected:
  friend class Singleton<MyClass>;
  MyClass() { x = 0; }
public:
  void setValue(int n) { x = n; }
  int getValue() const { return x; }
};

int main() {
  MyClass& m = MyClass::instance();
  cout << m.getValue() << endl;
  m.setValue(1);
  cout << m.getValue() << endl;
} ///:~

Listado 9.11. C10/CuriousSingleton.cpp


MyClass se convierte en Singleton:

1. Haciendo que su constructor sea private o protected.

2. Haciéndose amigo de Singleton<MyClass>.

3. Derivando MyClass desde Singleton<MyClass>.

La auto-referencia del paso 3 podría sonar inversímil, pero tal como se explicó en el Capítulo 5, funciona porque sólo hay una dependencia estática sobre el argumento plantilla de la plantilla Singleton. En otras palabras, el código de la clase Singleton<MyClass> puede ser instanciado por el compilador porque no depende del tamaño de MyClass. Es después, cuando se a Singleton<MyClass>::instance(), cuando se necesita el tamaño de MyClass, y para entonces MyClass ya se ha compilado y su tamaño se conoce.[139]

Es interesante lo intrincado que un patrón tan simple como el Singleton puede llegar a ser, y ni siquiera se han tratado todavía asuntos de seguridad de hilos. Por último, el patrón Singleton debería usarse lo justo y necesario. Los verdaderos objetos Singleton rara vez aparecen, y la última cosa para la que debe usarse un Singleton es para remplazar a una variable global. [140]