Singleton en C++

Esta entrada la dirijo al análisis de un patrón de diseño que me ha sido útil en varias ocasiones. El problema con éste es que, como comúnmente programo en C++, estaba dejando pasar por alto algo muy importante: el buen manejo de la memoria.

En Ingeniería de Software, el patrón Singleton es un patrón de diseño que restringe la instanciación de una clase a un único objeto. Esto es útil cuando se necesita exactamente un objeto para coordinar las acciones de todo el sistema.

Dicho lo anterior entremos en el tema.

Características del patrón Singleton

Un singleton normalmente se implementa con una clase que tiene las siguientes características:

  • Un atributo estático para la única instancia que tendrá el objeto (en C++ sería un apuntador a la misma clase).
  • Constructores privados.
  • Un método estático que retorne un apuntador o una referencia a la instancia de la clase.

Detalles de la implementación

Al principio la instancia del objeto debe apuntar al una dirección “null”, en en el caso de C++ se inicializa apuntando a nullptr. Una vez que el método es invocado, éste debe verificar si el objeto apunta a “null”, de ser así entonces solicita memoria para el objeto y finalmente lo retorna. Esto es para evitar tener el objeto en memoria si nunca es utilizado. Solamente será instanciado la primera vez que se use.

En la implementación que yo desarrollé tengo dos versiones del método: uno que retorna un apuntador a la instancia y uno que retorna una referencia a la instancia.

La versión que retorna el apuntador a la instancia se ve de la siguiente manera:


static Singleton * get_ptr_instance()
{
  if (instance == nullptr)
    instance = new Singleton;
  return instance;
}

Y la versión que retorna la referencia se apoya en el método anterior, ésta luce como sigue:


static Singleton & get_instance()
{
  return *get_ptr_instance();
}

Hay que tener en cuenta que esta última versión de método podría tornarse peligrosa, pues si se invoca de la siguiente manera:


Singleton s = Singleton::get_instance();

Se creará una copia de la instancia y violará la regla de una única instancia, pues en C++ si no se especifica el constructor copia y el operador =, éste provee una implementación por omisión de éstos. Para evitar este problema se plantean dos soluciones, una es es escribir el constructor copia y el operador = como privados o prohibirlos igualándolos a la palabra reservada “delete” en la declaración.

Mi grandioso error

Como es bien sabido, cuando se usa el operador “new” de C++, la memoria solicitada no se libera hasta que uno mismo la manda a liberar por medio del operador “delete” o hasta que se reinicie la máquina.

Mi gran error en la primera implementación fue llamar al operador “delete” dentro del destructor de la clase, es decir, tenía programado el siguiente destructor.


~Singleton()
{
  delete instance;
}

Bueno, el punto es que eso nunca liberaría la memoria. ¿Cuál fue mi confusión? Comúnmente cuando uno tiene un atributo dentro de una clase al cual se le asigna memoria dinámicamente dentro de ésta, hay que asegurarse de liberar la memoria en el destructor. La diferencia aquí es que no estamos hablando de un objeto cualquiera sino de un apuntador a la misma clase que lo contiene que además de eso será la única instancia existente.

El destructor de una clase se invoca cuando es invocado el operador “delete” sobre un apuntador o cuando un objeto sale de su ámbito. Ahora bien, ¿qué debería ocurrir para que se liberase la memoria de la instancia? Que el destructor fuese invocado, pero para que sea invocado debería, o bien, una instancia salir de ámbito o llamarse el operador “delete” sobre un puntero instanciado con “new”. En este caso como el Singleton fue instanciado mediante el operador “new”, es necesario hacer el llamado al operador “delete” explícitamente para que se pueda invocar el destructor. El caso es que como yo lo había implementado había creado una paradoja en la destrucción, pues nunca se llamaría el operador “delete” porque nunca se invocaría el destructor porque nunca se llamó a un “delete” (sí, una paradoja).

Al final de la ejecución del programa, lo que se libera automáticamente es el apuntador, es decir, el espacio de memoria que almacena la dirección reservada para la instancia, pero jamás liberaría la instancia como tal.

Después de que descubrí el error garrafal que cometí, llegué a la conclusión de que al final de la ejecución del programa es que se debe llamar al “delete” sobre el apuntador a la instancia de la siguiente manera:


delete Singleton::get_ptr_instance();

Esto plantea entonces dos posibles escenarios, y son:

  • ¿Qué pasará si el el objeto nunca es utilizado durante la ejecución del programa? La respuesta es, cuando se llame a “delete” sobre la consulta del apuntador a la instancia, ésta internamente utilizará el operador “new” para solicitar memoria para liberarla inmediatamente. Creo que eso no tiene sentido hacerlo.
  • El otro es que habría que asegurarse de llamar al operador “delete” en cualquier circunstancia que saque al programa de la ejecución para que la memoria no quede reservada.

Solución al problema de liberar la memoria

C++ provee 3 tipos de apuntadores “inteligentes” (smart pointers) los cuales se encargan de recibir un puntero a un espacio de memoria reservada y cuando el objeto sale de ámbito y se invoca su destructor, manda a liberar la memoria del puntero el cual maneja. Éstos son:

  • unique_ptr: Un objeto de estos maneja un apuntador y lo liberará cuando éste se destruya, no admite copias.
  • shared_ptr: Hace el mismo trabajo que unique_ptr con la salvedad de que se admiten copias y la memoria será liberada sólo cuando la última copia del shared_ptr sea destruída.
  • weak_ptr: Éstos sólo pueden existir como copia de un shared_ptr, la destrucción de un weak_ptr no afecta a los shared_ptr que mantengan copias del apuntador, sin embargo, cuando el último shared_ptr es destruído todas las copias de weak_ptr existentes quedarán inconsistentes.

Como el Singleton maneja una sola instancia, entonces decidí utilizar el unique_ptr, por lo que el apuntador a la instancia lo definí de esta forma:


static std::unique_ptr<Singleton> instance;

Y el método que retorna el apuntador a la instancia queda de la siguiente manera:


static Singleton * get_ptr_instance()
{
  if (instance.get() == nullptr)
    instance = std::unique_ptr<Singleton>(new Singleton);
  return instance;
}

Generalización del Singleton

En algunas aplicaciones que he desarrollado, he tenido que programar dos o tres diferentes clases bajo este patrón. Y es realmente fastidioso tener que programar el mismo modelo para cada una de éstas, así que decidí generalizarlo.

Mi primer intento de generalización fue crear una clase llamada puramente Singleton y que cualquier clase que tuviese este patrón heredará de ésta. El problema con este modelo fue que solamente heredaba la variable estática de instancia, tenía igualmente que escribir todos los constructores y los metodos que retornan las referencias para hacer el respectivo casting de clase padre a clase hija, pues al llamar a get_instance() me retorna un Singleton y allí no se manejan las variables y métodos de la clase hija.

En un segundo intento (y con este me quedé) la programé (digamos) a la inversa. Programé un Singleton que deriva de cualquier clase que requiera ser usada bajo este patrón. Así, cada vez que quiero reutilizar el patrón, lo que hago es escribir una clase con sus atributos y métodos (por ejemplo una clase llamada Mi_Singleton_Base) y debajo de toda la definición escribo la siguiente línea:


using Mi_Singleton = Singleton<Mi_Singleton_Base>

Así cada vez que quiera usarla, lo hago de la siguiente manera:


Mi_Singleton & ms = Mi_Singleton::get_instance();

Referencias:

Patrones de Diseño
Bjarne Stroustrup, The C++ Programming Language 4th ed

Creative Commons License
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Posted in C++, Patrones de diseño, Programación, Programación Genérica Tagged with: , , , ,

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

*