Le singleton
publié le mardi 13 mars 2007 (vu 1609 fois)

A travers cet article, nous allons découvrir un modèle de classe particulier en programmation C++, le singleton, afin de pouvoir en tirer avantage dans nos futurs projets.

Le singleton kézako ?
Un singleton n’est pas une classe mais un modèle de classe pour une utilisation particulière.
L’intérêt de définir une classe en singleton est de limiter le nombre d’instanciations de cette classe afin que celle-ci n’ai qu’un seul et unique exemplaire en mémoire (d’ou le terme singleton).

Vous vous demandez certainement concrêtement quel est l’intérêt du singleton, pourquoi limiter les instances d’une classe ?
Prenons comme exemple un programme qui doit se connecter à une base de données, se servant d’une classe encapsulant toutes les possibilités d’accès permises à cette base.
Cette classe peut très bien être définie globalement, une variable gardant en mémoire l’instance de la classe nous permettant d’y accéder comme bon nous semble.
Définir cette classe en tant que singleton permet par exemples :
- d’empêcher le programmeur de redéfinir une instance de la classe par erreur
- d’encapsuler à l’intérieur de la classe elle-même l’instance unique de la classe
- de rendre l’instance accessible de n’importe où dans le programme telle une variable globale


Les bases du singleton

Un bout de code étant bien plus parlant que de longues explications, voyons à quoi ressemble un singleton :

Rassurez vous, je ne vais pas vous laisser en plan avec ce bout de code qui, les plus avertis l’auront remarqué, ne fait rien.
La première chose qui doit vous sauter aux yeux est la protection du constructeur et du destructeur.
L’idée est simple, vous n’avez pas accès au constructeur, ni au destructeur. Ainsi vous ne pouvez créer d’objet de type CSingleton, ni en détruire. Seule la classe CSingleton peut le faire.
La méthode publique Instance permet à la fois de créer l’instance unique de la classe et de la récupérer. La présence d’une instance statique de la classe dans cette méthode suffit pour cela.
Le constructeur par copie comme l’opérateur de copie sont prohibés en étant déclarés protégés, comme le constructeur et le destructeur. Les déclarer suffit, les coder n’ont aucun intérêt puisqu’il s’agit ici d’empêcher de les utiliser.

Empêcher un quelconque programmeur de faire n’importe quoi avec votre singleton est un bien grand mot. Car évidemment, si un programmeur n’aime pas votre façon de faire et qu’il a accès à votre code source, rien ne l’empêche de le modifier pour faire ce qu’il veut.

Mais l’utilisation du singleton dans tout ça, comment ça se passe ?
Vous aurez remarqué que la méthode Instance est statique. Rien d’étonnant à cela puisque c’est cette méthode qui encapsule l’instance unique de notre classe. Instance unique qui se doit d’être accessible, telle une variable globale, depuis n’importe quelle partie de notre programme.
Comme toute méthode ou variable statique d’une classe, nous allons récupérer l’instance de la manière suivante :

Notre pauvre classe n’ayant pas d’autre méthode que Instance il vous est peut-être difficile de vous faire une idée précise du fonctionnement.
Essayons donc avec une classe un peu plus étoffée, une gestion fictive de base de données avec des noms d’utilisateurs et des mots de passe.

Tout d’abord notre fichier d’entête (DataBase.hpp) :

Nous avons là une classe similaire à notre CSingleton de tout à l’heure avec une méthode retournant un booléen sur la connection d’un utilisateur.
Dans notre classe, une map contient les noms des utilisateurs associés à leur mot de passe. Pour faire simple, notre constructeur va remplir cette map avec quelques utilisateurs.

Notre fichier source (DataBase.cpp) :

Notre main va tester la connection d’un utilisateur "Lambda" :

Voilà notre petit programme d’exemple prêt à être exécuté.
Voyons ce que nous obtenons dans la console de sortie :

Debut du programme Connection a la base de donnees User "Lambda" Pass "" : Utilisateur inconnu User "Omega" Pass "" : Utilisateur inconnu User "Alpha" Pass "" : Mot de passe utilisateur invalide Fin du programme Deconnection de la base de donnees

Analysons ligne par ligne ce qui se passe :
- Notre programme débute dans le main, comme n’importe quel programme le ferait
- Il se connecte à la base de données, provoqué par le premier appel à CDataBase ::Instance()
- L’utilisateur Lambda tente de se connecter, premier appel à ConnectUser
- L’utilisateur Omega tente de se connecter, second appel à ConnectUser
- L’utilisateur Alpha tente de se connecter
- Le programme se termine
- Destruction de l’instance de CDataBase qui se déconnecte de la base de données

On remarque tout d’abord que sur 3 appels à CDataBase ::Instance(), seul le premier d’entre eux a provoqué la connection à la base de données. Logique, le premier appel a provoqué la création de l’instance statique de la classe, tandis que pour les appels suivants, cette intance était déjà créée.
Je ne vous avait pas parlé de la destruction de la classe, comment cela se passe, comment on détruit l’instance quand on n’en a plus besoin. Cette variable est tout simplement détruite lorsque le programme se termine.
Pour résumer, la création de la classe se fait dès qu’on en a besoin, dès qu’on fait appel à elle, tandis que la destruction ne se fera qu’une fois le programme terminé, lorque tout ce qui a été créé sur la pile par notre programme sera détruit.

A cela, un problème pourrait très bien se poser. Non pas à la création, puisqu’il suffit de faire appel à Instance pour que notre classe soit initialisée dès le début de notre programme, mais à la destruction. Comment faire pour détruire notre instance avant la fin du programme ?
Tout d’abord quel intérêt de vouloir détruire l’instance "par vous même" ? Tout simplement lorsque notre programme utilise plusieurs singleton et que l’un d’entre eux nécessite d’être détruit avant un autre. En clair, on peut s’assurer facilement de l’ordre de la création de nos singleton mais pas de leur destruction.
C’est là qu’intervient la notion de Singleton Dynamique.


Le singleton dynamique

Tel que nous l’avons étudié précédemment, notre singleton est statique, créé lors du premier appel à la méthode statique Instance mais détruit lorsque le progrmme se termine.
Si nous voulons pouvoir contrôler l’ordre de destruction d’un singleton, il va faloir l’allouer dynamiquement.
Reprenons notre exemple de gestion fictive de base de données. Nous allons apporter les modifications nécessaires à notre fichier d’entête DataBase.hpp pour commencer.
Tout d’abord, nous avons besoin d’une donnée supplémentaire qui servira à stocker l’instance. Ajoutons donc la donnée suivante dans la classe :

Cette donnée est en statique pour les mêmes raisons que dans le singleton statique : une seule et unique instance pour CDataBase.

Toujours dans notre fichier d’entête, modifions la méthode Instance de la façon suivante :

La méthode Instance va créer l’instance si celle-ci n’existe pas déjà et retourne toujours une référence sur un CDataBase.

Comme la création de l’instance se fait maintenant de manière dynamique, il va nous faloir la possibilité de libérer cette resource.
Pour ce faire, nous ajoutons la méthode suivante à notre classe :

Les modifications du fichier d’entête nécessitent un seul ajout au fichier source DataBase.cpp :

Cette déclaration est nécessaire afin de définir m_pInstance et de l’initialiser.
Aucune modification du code existant.

Reste notre programme principal, le main.
Nous allons simplement ajouter, avant la fin de programme, la libération de notre singleton :

Et nous obtenons dans la console de sortie :

Debut du programme Connection a la base de donnees User "Lambda" Pass "" : Utilisateur inconnu User "Omega" Pass "" : Utilisateur inconnu User "Alpha" Pass "" : Mot de passe utilisateur invalide Deconnection de la base de donnees Fin du programme

Notre programme avec le singleton dynamique se déroule exactement de la mème manière qu’avec le singleton statique à un détail près. En effet, la déconnection à la base de données s’effectue avant que le programme ne se termine. Ce qui est exactement ce que nous voulions, n’est-ce pas ?


Avantages, inconvénients

Le principe du singleton nous permet d’encapsuler les parties spécifiques de notre programme, qu’elles soient indépendantes ou interdépendantes, tout en les laissant accessible à tout moment. L’avantage du singleton statique, c’est qu’on n’a pas à se soucier de le détruire puisque celui-ci se terminera avec notre programme. Cependant, comme nous l’avons vu, si une autre partie du programme doit se terminer avant notre singleton, il faudra lui préférer la forme dynamique afin de contrôler sa fermeture.
Un autre avantage de la gestion dynamique de notre singleton est la possibilité de le réinitialiser. L’appel à la méthode Release détruisant l’instance, le prochain appel à Instance réactivera le singleton dans un état initial.
Attention également à la lourdeur que l’abus de singleton peut engendrer. Utiliser de nombreux singleton pour gérer les différentes parties d’un moteur de jeu (affichage, textures, objets, resources, rendu sonore, ...) peut complexifier notre travail. L’idée d’un gestionnaire de singletons dynamiques, contrôlant l’ordre de leur création ainsi que de leur destruction, pourrait être intéressante ;-)

Code Source Singleton Statique (fichier Zip de 2.9 ko)
Code Source Singleton Dynamique (fichier Zip de 3 ko)
Executable Windows Singleton Statique (fichier Zip de 93.3 ko)
Executable Windows Singleton Dynamique (fichier Zip de 93.4 ko)