Singleton em C++: Como funciona por baixo dos panos? Análise da pergunta lendária

Treinador Online de Python para Iniciantes

Aprenda Python facilmente sem sobrecarga teórica. Resolva tarefas práticas com verificação automática e escreva código diretamente no navegador.

Start Course

Introdução: Por que Singleton não é apenas "um objeto"



Se você já deu uma olhada no Stack Overflow em busca de uma resposta para a pergunta: "How do you implement the Singleton design pattern?", viu milhares de votos, dezenas de opções e debates acalorados nos comentários. Essa pergunta se tornou um clássico porque Singleton é um dos padrões mais fáceis de entender, mas um dos mais traiçoeiros para implementar corretamente.



Neste artigo, não vamos apenas mostrar como escrever um Singleton em C++. Vamos olhar por baixo dos panos: analisar como a memória estática funciona, o que são garantias de inicialização, por que a abordagem ingênua pode matar a performance e como a versão final (Meyers Singleton) se tornou o padrão de facto.



Se você está se preparando para uma entrevista ou quer entender melhor C++, este artigo é para você. Vamos lá!



O que é Singleton? Um breve lembrete



Singleton é um padrão de projeto criacional que garante que uma classe tenha apenas uma instância e fornece um ponto de acesso global a ela.



Parece simples, certo? Mas em C++ existem nuances:

  • Thread safety na primeira criação do objeto.
  • Gerenciamento do tempo de vida (quando o objeto é destruído?).
  • Prevenção de cópia e atribuição.
  • Problemas com concorrência (data race).


Vamos examinar a evolução da implementação — da ingênua à industrial.



Implementação ingênua: O que há de errado?



A maneira mais simples é tornar o construtor privado e armazenar um ponteiro estático:



class Singleton {public:    static Singleton* getInstance() {        if (instance == nullptr) {            instance = new Singleton();        }        return instance;    }

void doSomething() { // lógica }

private: Singleton() {} // construtor privado Singleton(const Singleton&) = delete; // proibição de cópia Singleton& operator=(const Singleton&) = delete; // proibição de atribuição

static Singleton* instance;};

// Definição do membro estáticoSingleton* Singleton::instance = nullptr;


Problemas:

  1. Não é thread-safe. Se duas threads chamarem getInstance() simultaneamente, ambas verão nullptr e criarão dois objetos. Isso viola o invariante do Singleton.
  2. Vazamento de memória. O objeto criado com new não é deletado automaticamente. (Embora na prática a memória seja devolvida ao SO quando o processo termina, o destrutor não é chamado).
  3. Problema de inicialização estática. Se outro objeto estático tentar usar o Singleton antes de sua inicialização — comportamento indefinido (UB).


Pode-se adicionar um mutex, mas isso tornará cada chamada a getInstance() mais lenta, mesmo quando o objeto já foi criado. Ruim.



Double-Checked Locking (DCLP): Quase certo, mas perigoso



Uma otimização popular é verificar o ponteiro duas vezes: primeiro sem bloqueio, depois com bloqueio. Isso é chamado de Double-Checked Locking Pattern.



#include <mutex>

class Singleton {public: static Singleton* getInstance() { if (instance == nullptr) { // Primeira verificação (sem bloqueio) std::lock_guard<std::mutex> lock(mutex); if (instance == nullptr) { // Segunda verificação (com bloqueio) instance = new Singleton(); } } return instance; }

private: Singleton() {} Singleton(const Singleton&) = delete; Singleton& operator=(const Singleton&) = delete;

static Singleton* instance; static std::mutex mutex;};

Singleton* Singleton::instance = nullptr;std::mutex Singleton::mutex;<

Blogs

Book Recommendations