Singleton en C++ : Comment ça fonctionne sous le capot ? Analyse d'une question légendaire

Formateur Python en ligne pour débutants

Apprenez Python facilement sans surcharge théorique. Résolvez des tâches pratiques avec vérification automatique et écrivez du code directement dans le navigateur.

Start Course

Introduction : Pourquoi Singleton n'est pas simplement "un objet unique"



Si vous avez déjà jeté un œil sur Stack Overflow à la recherche d'une réponse à la question : "How do you implement the Singleton design pattern?", vous avez vu des milliers de votes, des dizaines de variantes et des débats houleux dans les commentaires. Cette question est devenue un classique car Singleton est l'un des patterns les plus simples à comprendre, mais l'un des plus délicats à implémenter correctement.



Dans cet article, nous ne nous contenterons pas de montrer comment écrire un Singleton en C++. Nous allons regarder sous le capot : analyser comment fonctionne la mémoire statique, ce que sont les garanties d'initialisation, pourquoi une approche naïve peut tuer les performances et comment la version finale (Meyers Singleton) est devenue le standard de facto.



Si vous vous préparez à un entretien ou souhaitez approfondir votre compréhension du C++, cet article est pour vous. C'est parti !



Qu'est-ce que Singleton ? Petit rappel



Singleton est un patron de conception de création qui garantit qu'une classe a un seul et unique exemplaire et fournit un point d'accès global à celui-ci.



En apparence, qu'est-ce qui pourrait mal tourner ? Mais en C++, il y a des nuances :

  • La sécurité des threads lors de la première création de l'objet.
  • La gestion de la durée de vie (quand l'objet est-il détruit ?).
  • La prévention de la copie et de l'affectation.
  • Les problèmes de concurrence (data race).


Examinons l'évolution de l'implémentation — de la version naïve à la version industrielle.



Implémentation naïve : Qu'est-ce qui cloche ?



La méthode la plus simple consiste à rendre le constructeur privé et à stocker un pointeur statique :



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

void doSomething() { // logique }

private: Singleton() {} // constructeur privé Singleton(const Singleton&) = delete; // interdiction de copie Singleton& operator=(const Singleton&) = delete; // interdiction d'affectation

static Singleton* instance;};

// Définition du membre statiqueSingleton* Singleton::instance = nullptr;


Problèmes :

  1. Non thread-safe. Si deux threads appellent getInstance() simultanément, ils verront tous deux nullptr et créeront deux objets. Cela viole l'invariant du Singleton.
  2. Fuite de mémoire. L'objet créé via new n'est pas supprimé automatiquement. (Bien qu'en pratique, la mémoire soit rendue à l'OS à la fin du processus, le destructeur n'est pas appelé).
  3. Problème d'initialisation statique. Si un autre objet statique tente d'utiliser le Singleton avant son initialisation — comportement indéfini (UB).


On pourrait ajouter un mutex, mais cela ralentirait chaque appel à getInstance(), même lorsque l'objet est déjà créé. Pas idéal.



Double-Checked Locking (DCLP) : Presque correct, mais dangereux



Une optimisation populaire consiste à vérifier le pointeur deux fois : d'abord sans verrouillage, puis avec verrouillage. C'est ce qu'on appelle le Double-Checked Locking Pattern.



#include <mutex>

class Singleton {public: static Singleton* getInstance() { if (instance == nullptr) { // Première vérification (sans verrouillage) std::lock_guard<std::mutex> lock(mutex); if (instance == nullptr) { // Deuxième vérification (avec verrouillage) 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