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:
- Não é thread-safe. Se duas threads chamarem
getInstance()simultaneamente, ambas verãonullptre criarão dois objetos. Isso viola o invariante do Singleton. - Vazamento de memória. O objeto criado com
newnão é deletado automaticamente. (Embora na prática a memória seja devolvida ao SO quando o processo termina, o destrutor não é chamado). - 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;<