Patrón de diseño C ++ Singleton


736

Recientemente me topé con la realización / implementación del patrón de diseño Singleton para C ++. Se ve así (lo he adoptado del ejemplo de la vida real):

// a lot of methods are omitted here
class Singleton
{
   public:
       static Singleton* getInstance( );
       ~Singleton( );
   private:
       Singleton( );
       static Singleton* instance;
};

De esta declaración puedo deducir que el campo de instancia se inicia en el montón. Eso significa que hay una asignación de memoria. Lo que no está completamente claro para mí es cuándo se va a desasignar exactamente la memoria. ¿O hay un error y pérdida de memoria? Parece que hay un problema en la implementación.

Mi pregunta principal es, ¿cómo lo implemento de la manera correcta?



10
Encontrará una gran discusión sobre cómo implementar un singleton, junto con la seguridad de subprocesos en C ++ en este documento. aristeia.com/Papers/DDJ%5FJul%5FAug%5F2004%5Frevised.pdf

106
@sbi: solo un Sith trata en términos absolutos. ¿Se puede resolver la gran mayoría de los problemas sin Singletons? Absolutamente. ¿Los Singletons causan problemas propios? Si. Sin embargo, no puedo decir honestamente que son malos , ya que el diseño se trata de considerar las compensaciones y comprender los matices de su enfoque.
derekerdmann

11
@derekerdmann: No dije que nunca necesitaras una variable global (y cuando la necesitas, un Singleton a veces es mejor). Lo que dije es que deberían usarse lo menos posible. Glorificar a Singleton como un patrón de diseño valioso da la impresión de que es bueno usarlo, en lugar de que sea un truco , lo que hace que el código sea difícil de entender, difícil de mantener y difícil de probar. Por eso publiqué mi comentario. Nada de lo que dijiste hasta ahora contradecía esto.
sbi

13
@sbi: Lo que dijiste fue "No los uses". No es mucho más razonable "deberían usarse lo menos posible" a los que luego se cambió, seguramente verá la diferencia.
jwd

Respuestas:


1106

En 2008, proporcioné una implementación en C ++ 98 del patrón de diseño Singleton que se evalúa de forma perezosa, se garantiza la destrucción, no es técnicamente seguro para subprocesos:
¿alguien puede proporcionarme una muestra de Singleton en c ++?

Aquí hay una implementación actualizada de C ++ 11 del patrón de diseño Singleton que se evalúa de forma diferida, se destruye correctamente y es segura para subprocesos .

class S
{
    public:
        static S& getInstance()
        {
            static S    instance; // Guaranteed to be destroyed.
                                  // Instantiated on first use.
            return instance;
        }
    private:
        S() {}                    // Constructor? (the {} brackets) are needed here.

        // C++ 03
        // ========
        // Don't forget to declare these two. You want to make sure they
        // are unacceptable otherwise you may accidentally get copies of
        // your singleton appearing.
        S(S const&);              // Don't Implement
        void operator=(S const&); // Don't implement

        // C++ 11
        // =======
        // We can use the better technique of deleting the methods
        // we don't want.
    public:
        S(S const&)               = delete;
        void operator=(S const&)  = delete;

        // Note: Scott Meyers mentions in his Effective Modern
        //       C++ book, that deleted functions should generally
        //       be public as it results in better error messages
        //       due to the compilers behavior to check accessibility
        //       before deleted status
};

Consulte este artículo sobre cuándo usar un singleton: (no es frecuente)
Singleton: cómo debe usarse

Consulte estos dos artículos sobre el orden de inicialización y cómo hacer frente:
Orden de inicialización de variables estáticas
Búsqueda de problemas de orden de inicialización estática de C ++

Consulte este artículo que describe la vida útil:
¿Cuál es la vida útil de una variable estática en una función C ++?

Consulte este artículo que analiza algunas implicaciones de subprocesos para singletons:
instancia Singleton declarada como variable estática del método GetInstance, ¿es segura para subprocesos?

Consulte este artículo que explica por qué el bloqueo de doble verificación no funcionará en C ++:
¿Cuáles son todos los comportamientos indefinidos comunes que un programador de C ++ debe conocer?
Dr. Dobbs: C ++ y los peligros del bloqueo doblemente verificado: Parte I


23
Buena respuesta. Pero debería tener en cuenta que esto no es seguro para subprocesos stackoverflow.com/questions/1661529/…
Varuna

44
@zourtney: Mucha gente no se da cuenta de lo que acabas de hacer :)
Johann Gerell

44
@MaximYegorushkin: cuando esto se destruye está muy bien definido (no hay ambigüedad). Ver: stackoverflow.com/questions/246564/…
Martin York

3
What irks me most though is the run-time check of the hidden boolean in getInstance()Esa es una suposición sobre la técnica de implementación. No tiene por qué suponerse que esté vivo. consulte stackoverflow.com/a/335746/14065 . Puede forzar una situación para que siempre esté viva (menos sobrecarga que Schwarz counter). Las variables globales tienen más problemas con el orden de inicialización (en todas las unidades de compilación) ya que no fuerza un orden. La ventaja de este modelo es 1) inicialización diferida. 2) Capacidad para hacer cumplir una orden (Schwarz ayuda pero es más feo). Sí, get_instance()es mucho más feo.
Martin York

3
@kol: No. No es el habitual. El hecho de que los principiantes copien y peguen código sin pensar no lo convierte en el habitual. Siempre debe mirar el caso de uso y asegurarse de que el operador de asignación haga lo que se espera. Copiar y pegar código te conducirá a errores.
Martin York

47

Al ser un Singleton, generalmente no desea que se destruya.

Será derribado y desasignado cuando el programa finalice, que es el comportamiento normal y deseado para un singleton. Si desea poder limpiarlo explícitamente, es bastante fácil agregar un método estático a la clase que le permita restaurarlo a un estado limpio y reasignarlo la próxima vez que lo use, pero eso está fuera del alcance de un singleton "clásico".


44
si nunca se llama explícitamente a la instancia estática Singleton *, ¿no se consideraría técnicamente una pérdida de memoria?
Andrew Garrison

77
No es más una pérdida de memoria que una simple declaración de una variable global.
ilya n.

15
Para aclarar algo ... las preocupaciones de "pérdida de memoria" frente a los singletons son completamente irrelevantes. Si tiene recursos con estado en los que importa el orden de deconstrucción, los singletons pueden ser peligrosos; pero toda la memoria es recuperada limpiamente por el sistema operativo al finalizar el programa ... anulando este punto totalmente académico en el 99.9% de los casos. Si quiere discutir la gramática de un lado a otro de lo que es y no es una "pérdida de memoria", está bien, pero tenga en cuenta que es una distracción de las decisiones de diseño reales.
jkerian

12
@jkerian: las pérdidas de memoria y la destrucción en el contexto de C ++ no se trata realmente de la pérdida de memoria. Realmente se trata de control de recursos. Si pierde memoria, no se llama al destructor y, por lo tanto, los recursos asociados con el objeto no se liberan correctamente. La memoria es solo el ejemplo simple que usamos cuando enseñamos programación, pero existen recursos mucho más complejos.
Martin York

77
@ Martin Estoy completamente de acuerdo contigo. Incluso si el único recurso es la memoria, todavía tendrá problemas para tratar de encontrar fugas REALES en su programa si tiene que leer una lista de fugas, filtrando las que "no importan". Es mejor limpiar todo esto para que cualquier herramienta que reporte fugas solo informe cosas que SON un problema.
Dolphin

38

Podrías evitar la asignación de memoria. Hay muchas variantes, todas tienen problemas en caso de entorno de subprocesos múltiples.

Prefiero este tipo de implementación (en realidad, no se dice correctamente que prefiero, porque evito los singletons tanto como sea posible):

class Singleton
{
private:
   Singleton();

public:
   static Singleton& instance()
   {
      static Singleton INSTANCE;
      return INSTANCE;
   }
};

No tiene asignación de memoria dinámica.


3
En algunos casos, esta inicialización diferida no es el patrón ideal a seguir. Un ejemplo es si el constructor del singleton asigna memoria del montón y desea que esa asignación sea predecible, por ejemplo, en un sistema integrado u otro entorno estrictamente controlado. Prefiero, cuando el patrón Singleton es el mejor patrón para usar, crear la instancia como miembro estático de la clase.
dma

3
Para muchos programas más grandes, especialmente aquellos con bibliotecas dinámicas. Cualquier objeto global o estático que no sea primitivo puede provocar segfaults / bloqueos al salir del programa en muchas plataformas debido a problemas de orden de destrucción en la descarga de bibliotecas. Esta es una de las razones por las que muchas convenciones de codificación (incluidas las de Google) prohíben el uso de objetos estáticos y globales no triviales.
obecalp

Parece que la instancia estática en dicha implementación tiene un enlace interno y tendrá copias únicas e independientes en diferentes unidades de traducción, lo que causará un comportamiento confuso e incorrecto. Pero vi muchas de esas implementaciones, ¿me estoy perdiendo algo?
FaceBro

¿Qué impide que el usuario asigne esto a múltiples objetos donde el compilador detrás de escena usa su propio constructor de copias?
Tony Tannous

19

La respuesta de @Loki Astari es excelente.

Sin embargo, hay veces con estática varios objetos donde tiene que ser capaz de garantizar que el producto único no será destruido hasta que todos los objetos estáticos que utilizan el producto único ya no lo necesitan.

En este caso, std::shared_ptrse puede usar para mantener vivo el singleton para todos los usuarios, incluso cuando se llama a los destructores estáticos al final del programa:

class Singleton
{
public:
    Singleton(Singleton const&) = delete;
    Singleton& operator=(Singleton const&) = delete;

    static std::shared_ptr<Singleton> instance()
    {
        static std::shared_ptr<Singleton> s{new Singleton};
        return s;
    }

private:
    Singleton() {}
};

9

Otra alternativa sin asignación: cree un singleton, digamos de clase C, según lo necesite:

singleton<C>()

utilizando

template <class X>
X& singleton()
{
    static X x;
    return x;
}

Ni esta ni la respuesta de Cătălin es automáticamente segura para subprocesos en C ++ actual, pero estará en C ++ 0x.


Actualmente bajo gcc es seguro para subprocesos (y lo ha sido por un tiempo).
Martin York

13
El problema con este diseño es que si se usa en varias bibliotecas. Cada biblioteca tiene su propia copia del singleton que usa esa biblioteca. Entonces ya no es un singleton.
Martin York

6

No encontré una implementación CRTP entre las respuestas, así que aquí está:

template<typename HeirT>
class Singleton
{
public:
    Singleton() = delete;

    Singleton(const Singleton &) = delete;

    Singleton &operator=(const Singleton &) = delete;

    static HeirT &instance()
    {
        static HeirT instance;
        return instance;
    }
};

Para usar simplemente hereda tu clase de esto, como: class Test : public Singleton<Test>


1
No se pudo hacer que esto funcione con C ++ 17 hasta que protegí el constructor predeterminado y '= default;'.
WFranczyk

6

La solución en la respuesta aceptada tiene un inconveniente significativo: se llama al destructor para el singleton después de que el control abandona el main() función. Puede haber problemas realmente, cuando algunos objetos dependientes se asignan dentro main.

Encontré este problema al intentar introducir un Singleton en la aplicación Qt. Decidí que todos mis cuadros de diálogo de configuración deben ser Singletons y adopté el patrón anterior. Desafortunadamente, la clase principal de Qt QApplicationse asignó en la pila en elmain función, y Qt prohíbe crear / destruir diálogos cuando no hay ningún objeto de aplicación disponible.

Es por eso que prefiero los singletons asignados al montón. Proporciono un método explícito init()y term()para todos los singletons y los llamo dentro main. Por lo tanto, tengo un control total sobre el orden de creación / destrucción de singletons, y también garantizo que se crearán singletons, sin importar si alguien llamó getInstance()o no.


2
Si se refiere a la respuesta actualmente aceptada, su primera afirmación es incorrecta. No se llama al destructor hasta que se destruyan todos los objetos de duración de almacenamiento estático.
Martin York

5

Aquí hay una implementación fácil.

#include <Windows.h>
#include <iostream>

using namespace std;


class SingletonClass {

public:
    static SingletonClass* getInstance() {

    return (!m_instanceSingleton) ?
        m_instanceSingleton = new SingletonClass : 
        m_instanceSingleton;
    }

private:
    // private constructor and destructor
    SingletonClass() { cout << "SingletonClass instance created!\n"; }
    ~SingletonClass() {}

    // private copy constructor and assignment operator
    SingletonClass(const SingletonClass&);
    SingletonClass& operator=(const SingletonClass&);

    static SingletonClass *m_instanceSingleton;
};

SingletonClass* SingletonClass::m_instanceSingleton = nullptr;



int main(int argc, const char * argv[]) {

    SingletonClass *singleton;
    singleton = singleton->getInstance();
    cout << singleton << endl;

    // Another object gets the reference of the first object!
    SingletonClass *anotherSingleton;
    anotherSingleton = anotherSingleton->getInstance();
    cout << anotherSingleton << endl;

    Sleep(5000);

    return 0;
}

Solo se crea un objeto y esta referencia de objeto se devuelve todas y cada una de las palabras posteriores.

SingletonClass instance created!
00915CB8
00915CB8

Aquí 00915CB8 es la ubicación de memoria del Objeto singleton, la misma durante la duración del programa pero (¡normalmente!) Diferente cada vez que se ejecuta el programa.

Nota: este no es un hilo seguro. Debe garantizar la seguridad del hilo.


5

Si desea asignar el objeto en el montón, ¿por qué no utilizar un puntero único? La memoria también se desasignará ya que estamos usando un puntero único.

class S
{
    public:
        static S& getInstance()
        {
            if( m_s.get() == 0 )
            {
              m_s.reset( new S() );
            }
            return *m_s;
        }

    private:
        static std::unique_ptr<S> m_s;

        S();
        S(S const&);            // Don't Implement
        void operator=(S const&); // Don't implement
};

std::unique_ptr<S> S::m_s(0);

3
En desuso en c ++ 11. unique_ptr se recomienda en su lugar. cplusplus.com/reference/memory/auto_ptr cplusplus.com/reference/memory/unique_ptr
Andrew

2
Esto no es seguro para subprocesos. Es mejor hacer m_sun local de staticde getInstance()e inicializar inmediatamente sin una prueba.
Galik

2

De hecho, probablemente se asigna desde el montón, pero sin las fuentes no hay forma de saberlo.

La implementación típica (tomada de algún código que ya tengo en emacs) sería:

Singleton * Singleton::getInstance() {
    if (!instance) {
        instance = new Singleton();
    };
    return instance;
};

... y confíe en que el programa se salga del alcance para limpiar después.

Si trabaja en una plataforma donde la limpieza debe realizarse manualmente, probablemente agregaría una rutina de limpieza manual.

Otro problema al hacerlo de esta manera es que no es seguro para subprocesos. En un entorno multiproceso, dos subprocesos podrían pasar por el "si" antes de que cualquiera tenga la oportunidad de asignar la nueva instancia (por lo que ambos lo harían). Esto todavía no es un gran problema si confía en la finalización del programa para limpiar de todos modos.


puede deducir, ya que puede ver que la variable de instancia es un puntero a la instancia de clase.
Artem Barger

3
No hay necesidad de asignar dinámicamente el singleton. De hecho, esta es una mala idea ya que no hay forma de desasignar automáticamente usando el diseño anterior. Dejarlo caer fuera del alcance no llama a los destructores y es simplemente vago.
Martin York

Puede desasignar automáticamente utilizando la función atexit. Eso es lo que hacemos (sin decir que es una buena idea)
Joe

2

¿Alguien ha mencionado std::call_oncey std::once_flag? La mayoría de los otros enfoques, incluido el bloqueo de doble verificación, están rotos.

Un problema importante en la implementación del patrón singleton es la inicialización segura. La única forma segura es proteger la secuencia de inicialización con barreras de sincronización. Pero esas barreras en sí mismas deben iniciarse de manera segura. std::once_flages el mecanismo para garantizar una inicialización segura garantizada.


2

Repasamos este tema recientemente en mi clase de EECS. Si desea ver las notas de clase en detalle, visite http://umich.edu/~eecs381/lecture/IdiomsDesPattsCreational.pdf

Hay dos maneras en que sé crear una clase Singleton correctamente.

Primera forma:

Impleméntelo de forma similar a como lo tiene en su ejemplo. En cuanto a la destrucción, "los Singletons generalmente perduran durante la ejecución del programa; la mayoría de los sistemas operativos recuperarán la memoria y la mayoría de los otros recursos cuando finalice un programa, por lo que hay un argumento para no preocuparse por esto".

Sin embargo, es una buena práctica limpiar al finalizar el programa. Por lo tanto, puede hacer esto con una clase auxiliar SingletonDestructor estática auxiliar y declarar eso como amigo en su Singleton.

class Singleton {
public:
  static Singleton* get_instance();

  // disable copy/move -- this is a Singleton
  Singleton(const Singleton&) = delete;
  Singleton(Singleton&&) = delete;
  Singleton& operator=(const Singleton&) = delete;
  Singleton& operator=(Singleton&&) = delete;

  friend class Singleton_destroyer;

private:
  Singleton();  // no one else can create one
  ~Singleton(); // prevent accidental deletion

  static Singleton* ptr;
};

// auxiliary static object for destroying the memory of Singleton
class Singleton_destroyer {
public:
  ~Singleton_destroyer { delete Singleton::ptr; }
};

Singleton_destroyer se creará al inicio del programa, y ​​"cuando el programa finaliza, todos los objetos globales / estáticos son destruidos por el código de apagado de la biblioteca en tiempo de ejecución (insertado por el enlazador), por lo que el destructor eliminará el Singleton, ejecutando su incinerador de basuras."

Segunda forma

Esto se llama Meyers Singleton, creado por el asistente de C ++ Scott Meyers. Simplemente defina get_instance () de manera diferente. Ahora también puede deshacerse de la variable miembro del puntero.

// public member function
static Singleton& Singleton::get_instance()
{
  static Singleton s;
  return s;
}

Esto es correcto porque el valor devuelto es por referencia y puede usar la .sintaxis en lugar de-> acceder a las variables miembro.

"El compilador crea automáticamente el código que crea 's' por primera vez a través de la declaración, no después, y luego elimina el objeto estático al finalizar el programa".

Tenga en cuenta también que con el Singleton de Meyers "puede entrar en una situación muy difícil si los objetos dependen unos de otros en el momento de la terminación: ¿cuándo desaparece el Singleton en relación con otros objetos? Pero para aplicaciones simples, esto funciona bien".


1

Además de la otra discusión aquí, vale la pena señalar que puede tener globalidad, sin limitar el uso a una instancia. Por ejemplo, considere el caso de referencia contando algo ...

struct Store{
   std::array<Something, 1024> data;
   size_t get(size_t idx){ /* ... */ }
   void incr_ref(size_t idx){ /* ... */}
   void decr_ref(size_t idx){ /* ... */}
};

template<Store* store_p>
struct ItemRef{
   size_t idx;
   auto get(){ return store_p->get(idx); };
   ItemRef() { store_p->incr_ref(idx); };
   ~ItemRef() { store_p->decr_ref(idx); };
};

Store store1_g;
Store store2_g; // we don't restrict the number of global Store instances

Ahora, en algún lugar dentro de una función (como main) puede hacer:

auto ref1_a = ItemRef<&store1_g>(101);
auto ref2_a = ItemRef<&store2_g>(201); 

Los árbitros no necesitan almacenar un puntero en sus respectivos Storeporque esa información se proporciona en tiempo de compilación. Tampoco tiene que preocuparse por la Storevida útil del compilador porque el compilador requiere que sea global. Si de hecho solo hay una instancia, Storeentonces no hay sobrecarga en este enfoque; con más de una instancia, depende del compilador ser inteligente sobre la generación de código. Si es necesario, la ItemRefclase incluso se puede hacer unfriend deStore (¡puedes tener amigos con plantilla!).

Si en Storesí es una clase con plantilla, las cosas se vuelven más complicadas, pero aún es posible usar este método, tal vez implementando una clase auxiliar con la siguiente firma:

template <typename Store_t, Store_t* store_p>
struct StoreWrapper{ /* stuff to access store_p, e.g. methods returning 
                       instances of ItemRef<Store_t, store_p>. */ };

El usuario ahora puede crear un StoreWrappertipo (y una instancia global) para cada Storeinstancia global , y siempre acceder a las tiendas a través de su instancia de contenedor (olvidando así los detalles sangrientos de los parámetros de plantilla necesarios para usar Store).


0

Se trata de la gestión del tiempo de vida del objeto. Supongamos que tiene más de singletons en su software. Y dependen de Logger Singleton. Durante la destrucción de la aplicación, suponga que otro objeto singleton usa Logger para registrar sus pasos de destrucción. Debe garantizar que Logger se debe limpiar en último lugar. Por lo tanto, consulte también este documento: http://www.cs.wustl.edu/~schmidt/PDF/ObjMan.pdf


0

Mi implementación es similar a la de Galik. La diferencia es que mi implementación permite que los punteros compartidos limpien la memoria asignada, en lugar de retener la memoria hasta que la aplicación salga y los punteros estáticos se limpien.

#pragma once

#include <memory>

template<typename T>
class Singleton
{
private:
  static std::weak_ptr<T> _singleton;
public:
  static std::shared_ptr<T> singleton()
  {
    std::shared_ptr<T> singleton = _singleton.lock();
    if (!singleton) 
    {
      singleton.reset(new T());
      _singleton = singleton;
    }

    return singleton;
  }
};

template<typename T>
std::weak_ptr<T> Singleton<T>::_singleton;

0

Su código es correcto, excepto que no declaró el puntero de instancia fuera de la clase . Las declaraciones de clase estática de variables estáticas no se consideran declaraciones en C ++, sin embargo, esto está permitido en otros lenguajes como C # o Java, etc.

class Singleton
{
   public:
       static Singleton* getInstance( );
   private:
       Singleton( );
       static Singleton* instance;
};
Singleton* Singleton::instance; //we need to declare outside because static variables are global

Debe saber que la instancia de Singleton no necesita ser eliminada manualmente por nosotros . Necesitamos un solo objeto en todo el programa, por lo que al final de la ejecución del programa, se desasignará automáticamente.


-1

El documento al que se enlazó anteriormente describe la deficiencia del bloqueo de doble verificación es que el compilador puede asignar la memoria para el objeto y establecer un puntero a la dirección de la memoria asignada, antes de que se llame al constructor del objeto. Sin embargo, es bastante fácil en c ++ usar asignadores para asignar la memoria manualmente, y luego usar una llamada de construcción para inicializar la memoria. Usando este appraoch, el bloqueo de doble verificación funciona bien.


2
Lamentablemente no. Esto ha sido discutido en profundidad por algunos de los mejores desarrolladores de C ++ que existen. El bloqueo doblemente verificado está roto en C ++ 03.
Martin York

-1
#define INS(c) private:void operator=(c const&){};public:static c& I(){static c _instance;return _instance;}

Ejemplo:

   class CCtrl
    {
    private:
        CCtrl(void);
        virtual ~CCtrl(void);

    public:
        INS(CCtrl);

-1

Clase singleton simple, este debe ser su archivo de clase de encabezado

#ifndef SC_SINGLETON_CLASS_H
#define SC_SINGLETON_CLASS_H

class SingletonClass
{
    public:
        static SingletonClass* Instance()
        {
           static SingletonClass* instance = new SingletonClass();
           return instance;
        }

        void Relocate(int X, int Y, int Z);

    private:
        SingletonClass();
        ~SingletonClass();
};

#define sSingletonClass SingletonClass::Instance()

#endif

Acceda a su singleton de esta manera:

sSingletonClass->Relocate(1, 2, 5);

-3

Creo que deberías escribir una función estática en la que se elimine tu objeto estático. Debe llamar a esta función cuando esté a punto de cerrar su aplicación. Esto asegurará que no tenga pérdidas de memoria.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.