O padrão de projeto Singleton assegura que uma classe tenha apenas uma instância e fornece um ponto de acesso global a ela. Essa abordagem é particularmente útil quando um único objeto é requerido para coordenar operações em todo o sistema, ou quando a instância única precisa ser extensível através de subclasses, permitindo que os clientes utilizem a versão estendida sem alterações no código-fonte.
A responsabilidade principal recai sobre a própria classe, que deve encapsular a criação de sua instância e controlar o acesso a ela, geralmente através de um método estático. O construtor é deifnido como protegido ou privado para impedir a instanciação externa direta.
Implementação com Inicialização Tardia
A inicialização tardia (lazy initialization) posterga a criação do objeto até o momento em que ele é requisitado pela primeira vez. No exemplo abaixo, utilizamos um gerador de terrenos para um motor de jogos para ilustrar essa abordagem:
class TerrainGenerator {
public:
static TerrainGenerator* getInstance();
protected:
TerrainGenerator() = default;
private:
static TerrainGenerator* uniqueInstance;
};
TerrainGenerator* TerrainGenerator::uniqueInstance = nullptr;
TerrainGenerator* TerrainGenerator::getInstance() {
if (uniqueInstance == nullptr) {
uniqueInstance = new TerrainGenerator();
}
return uniqueInstance;
}
Em cenários onde o tipo exato da instância depende de configurações externas, podemos expandir a lógica de criação para instanciar subclasses específicas baseadas em variáveis de ambiente:
#include <cstdlib>
#include <string>
TerrainGenerator* TerrainGenerator::getInstance() {
if (uniqueInstance == nullptr) {
const char* terrainType = std::getenv("TERRAIN_TYPE");
std::string envConfig = terrainType ? terrainType : "";
if (envConfig == "volcanic") {
uniqueInstance = new VolcanicTerrainGenerator();
} else if (envConfig == "arctic") {
uniqueInstance = new ArcticTerrainGenerator();
} else {
uniqueInstance = new TerrainGenerator();
}
}
return uniqueInstance;
}
Concorrência e Segurança de Threads
A implementação tardia básica apresenta uma falha crítica em ambientes multithread: múltiplas threads podem avaliar a condição nula simultaneamente, resultando na criação de múltiplas instâncias. Para mitigar isso, empregamos a técnica de bloqueio de dupla verificação (Double-Checked Locking) com mutexes:
#include <mutex>
class TerrainGenerator {
public:
static TerrainGenerator* getInstance();
protected:
TerrainGenerator() = default;
private:
static TerrainGenerator* uniqueInstance;
static std::mutex instantiationMutex;
};
TerrainGenerator* TerrainGenerator::uniqueInstance = nullptr;
std::mutex TerrainGenerator::instantiationMutex;
TerrainGenerator* TerrainGenerator::getInstance() {
if (uniqueInstance == nullptr) {
std::lock_guard<std::mutex> lock(instantiationMutex);
if (uniqueInstance == nullptr) {
uniqueInstance = new TerrainGenerator();
}
}
return uniqueInstance;
}
Inicialização Antecipada
Alternativamente, a inicialização antecipada (eager initialization) cria a instância no momento da carga da classe, antes da execução da função principal. Como a inicialização de variáveis estáticas globais é gerenciada pelo thread principal antes de qualquer concorrência, essa abordagem é intrinsecamente segura contra condições de corrida, eliminando a sobrecarga de bloqueios em tempo de execução:
TerrainGenerator* TerrainGenerator::uniqueInstance = new TerrainGenerator();
TerrainGenerator* TerrainGenerator::getInstance() {
return uniqueInstance;
}