Padrão de design C ++ Singleton


736

Recentemente, deparei-me com a realização / implementação do padrão de design Singleton para C ++. Foi assim (eu o adotei no exemplo da vida real):

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

A partir desta declaração, posso deduzir que o campo da instância é iniciado no heap. Isso significa que há uma alocação de memória. O que não está totalmente claro para mim é quando exatamente a memória será desalocada? Ou há um erro e um vazamento de memória? Parece que há um problema na implementação.

Minha principal pergunta é: como implementá-lo da maneira correta?



10
Você encontrará uma ótima discussão sobre como implementar um singleton, juntamente com a segurança de threads em C ++ neste documento. aristeia.com/Papers/DDJ%5FJul%5FAug%5F2004%5Frevised.pdf

106
@sbi - Apenas um Sith lida com absolutos. A grande maioria dos problemas pode ser resolvida sem Singletons? Absolutamente. Singletons causam problemas próprios? Sim. No entanto, não posso dizer honestamente que eles são ruins , pois o design tem tudo a ver com as vantagens e desvantagens da compreensão de sua abordagem.
22411 derekerdmann

11
@derekerdmann: Eu não disse que você nunca precisa de uma variável global (e quando você precisa de um, um Singleton às vezes é melhor). O que eu disse é que eles devem ser usados ​​o menos possível. Glorificar Singleton como um padrão de design valioso dá a impressão de que é bom usá-lo, em vez de ser um hack , tornando o código difícil de entender, difícil de manter e difícil de testar. Foi por isso que postei meu comentário. Nada do que você disse até agora contradiz isso.
SBI

13
@bi: O que você disse foi "Não os use". Não é o mais razoável "eles devem ser usados ​​o menos possível" que você mais tarde mudou para - certamente você vê a diferença.
jwd

Respostas:


1106

Em 2008, forneci uma implementação em C ++ 98 do padrão de design Singleton que é avaliado preguiçosamente, com destruição garantida e não é tecnicamente seguro para threads:
Alguém pode me fornecer uma amostra de Singleton em c ++?

Aqui está uma implementação atualizada do C ++ 11 do padrão de design Singleton que é avaliada preguiçosamente, destruída corretamente e segura para threads .

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 artigo sobre quando usar um singleton: (não frequentemente)
Singleton: como ele deve ser usado

Consulte este dois artigos sobre ordem de inicialização e como lidar com:
Ordem de inicialização de variáveis ​​estáticas
Localizando problemas de ordem de inicialização estática em C ++

Consulte este artigo descrevendo as vidas úteis:
Qual é a vida útil de uma variável estática em uma função C ++?

Consulte este artigo que discute algumas implicações de encadeamento para singletons: A
instância Singleton declarada como variável estática do método GetInstance, é segura para threads?

Consulte este artigo que explica por que o bloqueio com verificação dupla não funciona no C ++:
Quais são os comportamentos indefinidos comuns que um programador de C ++ deve conhecer?
Dr. Dobbs: C ++ e os perigos do bloqueio duplo: parte I


23
Boa resposta. Mas observe que isso não é seguro para threads stackoverflow.com/questions/1661529/…
Varuna

4
@zourtney: Muitas pessoas não percebem o que você fez :)
Johann Gerell

4
@MaximYegorushkin: Quando isso é destruído, é muito bem definido (não há ambiguidade). Veja: stackoverflow.com/questions/246564/…
Martin York

3
What irks me most though is the run-time check of the hidden boolean in getInstance()Essa é uma suposição sobre a técnica de implementação. Não é necessário supor que esteja vivo. consulte stackoverflow.com/a/335746/14065 Você pode forçar uma situação para que ela esteja sempre viva (menos sobrecarga que Schwarz counter). As variáveis ​​globais têm mais problemas com a ordem de inicialização (entre as unidades de compilação), pois você não força uma ordem. A vantagem deste modelo é 1) inicialização lenta. 2) Capacidade de fazer cumprir uma ordem (Schwarz ajuda, mas é mais feio). Sim, get_instance()é muito mais feio.
Martin York

3
@ kol: Não. Não é o habitual. Só porque os iniciantes copiam e colam o código sem pensar, não o torna usual. Você sempre deve examinar o caso de uso e garantir que o operador de atribuição faça o que é esperado. O código de copiar e colar levará você a erros.
Martin York

47

Sendo um Singleton, você geralmente não quer que seja destruído.

Ele será desmembrado e desalocado quando o programa terminar, que é o comportamento normal e desejado para um singleton. Se você deseja limpá-lo explicitamente, é bastante fácil adicionar um método estático à classe que permita restaurá-lo para um estado limpo e realocá-lo na próxima vez em que for usado, mas isso está fora do escopo de um singleton "clássico".


4
se a exclusão nunca for chamada explicitamente na instância estática de Singleton *, isso ainda não seria tecnicamente considerado um vazamento de memória?
Andrew Garrison

7
Não é mais um vazamento de memória do que uma simples declaração de uma variável global.
ilya n.

15
Para esclarecer uma coisa ... preocupações com "vazamento de memória" em relação a singletons são completamente irrelevantes. Se você possui recursos estatais nos quais a ordem de desconstrução é importante, os singletons podem ser perigosos; mas toda a memória é recuperada de forma limpa pelo sistema operacional na finalização do programa ... anulando esse ponto totalmente acadêmico em 99,9% dos casos. Se você quiser discutir a gramática do que é e não é um "vazamento de memória", tudo bem, mas saiba que isso é uma distração das decisões reais de design.
jkerian

12
@ jkerian: vazamentos de memória e destruição no contexto C ++ não são realmente sobre o vazamento de memória. Realmente, é sobre controle de recursos. Se você vazar memória, o destroctor não será chamado e, portanto, quaisquer recursos associados ao objeto não serão liberados corretamente. A memória é apenas o exemplo simples que usamos ao ensinar programação, mas existem recursos muito mais complexos por aí.
Martin Iorque

7
@ Martin Concordo plenamente com você. Mesmo que o único recurso seja a memória, você ainda terá problemas ao tentar encontrar vazamentos REAIS no seu programa se precisar percorrer uma lista de vazamentos, filtrando os que "não importam". É melhor limpar tudo isso, para que qualquer ferramenta que relate vazamentos relate apenas coisas que SÃO um problema.
21410 Dolphin

38

Você pode evitar a alocação de memória. Existem muitas variantes, todas com problemas em caso de ambiente multithreading.

Eu prefiro esse tipo de implementação (na verdade, não se diz corretamente que prefiro, porque evito singletons o máximo possível):

class Singleton
{
private:
   Singleton();

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

Não possui alocação de memória dinâmica.


3
Em alguns casos, essa inicialização lenta não é o padrão ideal a seguir. Um exemplo é se o construtor do singleton aloca memória da pilha e você deseja que essa alocação seja previsível, por exemplo, em um sistema incorporado ou em outro ambiente fortemente controlado. Eu prefiro, quando o padrão Singleton é o melhor padrão para usar, criar a instância como um membro estático da classe.
dma

3
Para muitos programas maiores, especialmente aqueles com bibliotecas dinâmicas. Qualquer objeto global ou estático que não seja primitivo pode levar a falhas / falhas após a saída do programa em muitas plataformas devido a problemas de ordem de destruição ao descarregar as bibliotecas. Essa é uma das razões pelas quais muitas convenções de codificação (incluindo o Google) proíbem o uso de objetos estáticos e globais não triviais.
219 obecalp

Parece que a instância estática nessa implementação tem ligação interna e terá cópias únicas e independentes em diferentes unidades de tradução, o que causará um comportamento confuso e errado. Mas eu vi muitas dessas implementações, estou perdendo alguma coisa?
FaceBro

O que impede o usuário de atribuir isso a vários objetos em que o compilador nos bastidores usa seu próprio construtor de cópias?
Tony Tannous

19

Resposta de @Loki Astari é excelente.

No entanto, há momentos com vários objetos estáticos nos quais você precisa garantir que o singleton não será destruído até que todos os seus objetos estáticos que usam o singleton não precisem mais dele.

Nesse caso, std::shared_ptrpode ser usado para manter o singleton ativo para todos os usuários, mesmo quando os destruidores estáticos estão sendo chamados no final do 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

Outra alternativa sem alocação: crie um singleton, digamos, de classe C, conforme necessário:

singleton<C>()

usando

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

Nem esta nem a resposta de Cătălin são automaticamente seguras contra threads no C ++ atual, mas estarão no C ++ 0x.


Atualmente no gcc, ele é seguro para threads (e já existe há algum tempo).
Martin Iorque

13
O problema com esse design é que, se usado em várias bibliotecas. Cada biblioteca possui uma cópia própria do singleton que essa biblioteca usa. Portanto, não é mais um singleton.
Martin Iorque

6

Eu não encontrei uma implementação de CRTP entre as respostas, então aqui 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 apenas herde sua classe disso, como: class Test : public Singleton<Test>


1
Não foi possível fazer isso funcionar com o C ++ 17 até que eu tenha protegido o construtor padrão e '= default;'.
WFranczyk

6

A solução na resposta aceita tem uma desvantagem significativa - o destruidor do singleton é chamado depois que o controle sai da main()função. Pode haver realmente problemas quando alguns objetos dependentes são alocados dentromain .

Encontrei esse problema ao tentar introduzir um Singleton no aplicativo Qt. Decidi que todas as minhas caixas de diálogo de configuração deveriam ser Singletons e adotou o padrão acima. Infelizmente, a classe principal do Qt QApplicationfoi alocada na pilha nomain função, e o Qt proíbe a criação / destruição de diálogos quando nenhum objeto de aplicativo está disponível.

É por isso que eu prefiro singletons alocados em heap. Eu forneço um explícito init()e term()métodos para todos os singletons e os chamo por dentro main. Assim, eu tenho um controle total sobre a ordem de criação / destruição de singletons, e também garanto que os singletons serão criados, independentemente de alguém ligar getInstance()ou não.


2
Se você está se referindo à resposta atualmente aceita, sua primeira declaração está errada. O destruidor não é chamado até que todos os objetos de duração de armazenamento estático sejam destruídos.
Martin York

5

Aqui está uma implementação 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;
}

Somente um objeto criado e essa referência de objeto são retornados sempre e sempre.

SingletonClass instance created!
00915CB8
00915CB8

Aqui 00915CB8 é o local da memória do objeto singleton, o mesmo para a duração do programa, mas (normalmente!) É diferente cada vez que o programa é executado.

NB Este não é um fio seguro. Você deve garantir a segurança do fio.


5

Se você deseja alocar o objeto na pilha, por que não usar um ponteiro exclusivo. A memória também será desalocada, pois estamos usando um ponteiro exclusivo.

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
Descontinuado em c ++ 11. O unique_ptr é recomendado. cplusplus.com/reference/memory/auto_ptr cplusplus.com/reference/memory/unique_ptr
Andrew

2
Este não é um tópico seguro. Melhor fazer m_sa um local staticde getInstance()e inicializar-lo imediatamente, sem um teste.
Galik

2

Na verdade, é provavelmente alocado a partir da pilha, mas sem as fontes não há como saber.

A implementação típica (tirada de algum código que eu já tenho no emacs) seria:

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

... e confie no programa que está fora do escopo para limpar depois.

Se você trabalha em uma plataforma onde a limpeza deve ser feita manualmente, provavelmente adicionaria uma rotina de limpeza manual.

Outro problema ao fazê-lo dessa maneira é que não é seguro para threads. Em um ambiente multithread, dois threads podem passar pelo "if" antes que um deles tenha a chance de alocar a nova instância (para que ambos o fizessem). Isso ainda não é grande coisa se você estiver dependendo da finalização do programa para limpar de qualquer maneira.


você pode deduzir, pois pode ver que a variável de instância é um ponteiro para a instância da classe.
Artem Barger

3
Não há necessidade de alocar dinamicamente o singleton. De fato, é uma péssima idéia, pois não há como desalocar automaticamente usando o design acima. Deixar cair fora do escopo é que não chama destruidores e é apenas preguiçoso.
Martin Iorque

Você pode desalocar automaticamente usando a função atexit. Isso é o que fazemos (não dizendo que é uma boa idéia)
Joe

2

Alguém mencionou std::call_oncee std::once_flag? A maioria das outras abordagens - incluindo o bloqueio com verificação dupla - está quebrada.

Um grande problema na implementação de padrões singleton é a inicialização segura. A única maneira segura é proteger a sequência de inicialização com barreiras de sincronização. Mas essas barreiras precisam ser iniciadas com segurança. std::once_flagé o mecanismo para obter uma inicialização segura garantida.


2

Analisamos esse tópico recentemente na minha classe EECS. Se você quiser ver as notas da aula em detalhes, visite http://umich.edu/~eecs381/lecture/IdiomsDesPattsCreational.pdf

Existem duas maneiras de criar uma classe Singleton corretamente.

Primeira maneira:

Implemente de maneira semelhante à do seu exemplo. Quanto à destruição, "os singletons geralmente perduram pela duração da execução do programa; a maioria dos sistemas operacionais recupera memória e a maioria dos outros recursos quando um programa é finalizado; portanto, há um argumento para não se preocupar com isso".

No entanto, é uma boa prática limpar no final do programa. Portanto, você pode fazer isso com uma classe SingletonDestructor estática auxiliar e declarar isso como amigo no seu 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; }
};

O Singleton_destroyer será criado na inicialização do programa e "quando o programa terminar, todos os objetos globais / estáticos serão destruídos pelo código de desligamento da biblioteca de tempo de execução (inserido pelo vinculador); portanto, o_destroyer será destruído; seu destruidor excluirá o Singleton, executando seu destruidor."

Segunda Via

Isso é chamado de Meyers Singleton, criado pelo assistente de C ++, Scott Meyers. Simplesmente defina get_instance () de maneira diferente. Agora você também pode se livrar da variável de membro do ponteiro.

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

Isso é legal porque o valor retornado é por referência e você pode usar a .sintaxe em vez de ->acessar variáveis ​​de membro.

"O compilador cria automaticamente o código que cria 's' pela primeira vez através da declaração, e não depois e exclui o objeto estático na finalização do programa."

Observe também que, com o Meyers Singleton, "você pode entrar em uma situação muito difícil se os objetos confiarem um no outro no momento do término - quando o Singleton desaparece em relação a outros objetos? Mas, para aplicativos simples, isso funciona bem".


1

Além da outra discussão aqui, pode ser interessante notar que você pode ter globalidade, sem limitar o uso a uma instância. Por exemplo, considere o caso de referência 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

Agora, em algum lugar dentro de uma função (como main), você pode fazer:

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

Os árbitros não precisam armazenar um ponteiro de volta nos respectivos, Storeporque essas informações são fornecidas em tempo de compilação. Você também não precisa se preocupar com a Storevida útil do compilador, porque o compilador exige que seja global. Se, de fato, existe apenas uma instância Store, não há sobrecarga nessa abordagem; com mais de uma instância, cabe ao compilador ser inteligente sobre a geração de código. Se necessário, a ItemRefclasse pode até ser feita frienddeStore (você pode ter amigos incríveis!).

Se Storeela própria é uma classe de modelo, as coisas ficam mais confusas, mas ainda é possível usar esse método, talvez implementando uma classe auxiliar com a seguinte assinatura:

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>. */ };

O usuário agora pode criar um StoreWrappertipo (e instância global) para cada Storeinstância global e sempre acessar as lojas por meio da instância do wrapper (esquecendo os detalhes sangrentos dos parâmetros do modelo necessários para o uso Store).


0

Trata-se do gerenciamento do tempo de vida do objeto. Suponha que você tenha mais de singletons em seu software. E eles dependem do logger singleton. Durante a destruição do aplicativo, suponha que outro objeto singleton use o Logger para registrar suas etapas de destruição. Você deve garantir que o Logger seja limpo por último. Portanto, verifique também este documento: http://www.cs.wustl.edu/~schmidt/PDF/ObjMan.pdf


0

Minha implementação é semelhante à de Galik. A diferença é que minha implementação permite que os ponteiros compartilhados limpem a memória alocada, em vez de manter a memória até que o aplicativo seja encerrado e os ponteiros estáticos sejam limpos.

#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

Seu código está correto, exceto que você não declarou o ponteiro da instância fora da classe . As declarações de classe estática de variáveis ​​estáticas não são consideradas declarações em C ++, no entanto, isso é permitido em outras linguagens como C # ou 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

Você deve saber que a instância Singleton não precisa ser excluída manualmente por nós . Precisamos de um único objeto em todo o programa; assim, no final da execução do programa, ele será desalocado automaticamente.


-1

O documento que foi vinculado acima descreve a falha do bloqueio com verificação dupla: o compilador pode alocar a memória para o objeto e definir um ponteiro para o endereço da memória alocada, antes que o construtor do objeto seja chamado. No entanto, é bastante fácil no c ++ usar alocadores para alocar a memória manualmente e, em seguida, usar uma chamada de construção para inicializar a memória. Usando essa abordagem, o bloqueio verificado duas vezes funciona perfeitamente.


2
Infelizmente não. Isso foi discutido em profundidade por alguns dos melhores desenvolvedores de C ++ existentes. O bloqueio verificado duas vezes está quebrado no C ++ 03.
Martin York

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

Exemplo:

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

    public:
        INS(CCtrl);

-1

Classe singleton simples, este deve ser seu arquivo de classe de cabeçalho

#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

Acesse seu singleton assim:

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

-3

Eu acho que você deve escrever uma função estática em que seu objeto estático é excluído. Você deve chamar esta função quando estiver prestes a fechar seu aplicativo. Isso garantirá que você não tenha vazamento de memória.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.