Singleton – C++

by dinosaure

Pour ceux qui ont regardé mon C++ Blog et qui ont certaines compétences, ils ont peut être remarquer un design pattern du nom de Singleton. Néanmoins, pas tout le monde à ce savoir et il peut être intéressant de s’intéresser là dessus pour les petits novices qui lisent mon blog. Il faut déjà savoir ce qu’est un design pattern ou un patron de conception. En gros, les informaticiens ont remarqué qu’il y avait des problèmes récurrents en programmation objet (je parle bien du paradigme orienté objet, hein). Ils ont donc défini plusieurs patrons (des exemples en gros) pour aider l’apprenti à résoudre certains de ses problèmes. Il y a plusieurs patrons et ici, nous allons en voir qu’un seul. Néanmoins, je vous invite à voir les autres design pattern pour voir si vous pouvez optimiser (c’est pas vraiment le mot, on devrait dire plutôt « améliorer la structure de votre logiciel », mais ça fait bien) votre logiciel. Notez que ici, on parle plus sur l’architecture de votre logiciel que sur des notions du langage ou des notions sur les algorithmes. En effet, le design pattern n’a pas pour ambition d’être appris par cœur mais de comprendre son fonctionnement pour ensuite l’appliquer à des problèmes de conception que vous pouvez rencontrer dans votre développement. Ensuite, il vous suffira d’une recherche pour connaître sa structure dans votre langage préféré (du moment que celui-ci ce base sur le paradigme orienté objet).

Dans notre cas, nous allons utilisé le C++ car j’ai envie. Nous allons apprendre le fonctionnement du design pattern Singleton, pourquoi on l’utilise, quand on l’utilise et comment on l’utilise. Toute ces questions seront décrites ici pour que vous puissiez appréhender vos problèmes d’architecture plus rapidement (et gagner ainsi en productivité). Cela permettra en outre de maintenir votre projet avec plus de flexibilité (ces design pattern ont été pensé pour).

Alors commençons … C’est quoi un Singleton. C’est un patron qui permet de restreindre l’instanciation d’une classe à un seul objet. Par exemple, vous avez créer une classe Database, mais vous voulez que dans tout votre programme, il n’y est qu’un seul et unique objet Database accessible partout (du moment que la classe est défini). L’intérêt, c’est de pouvoir centraliser les opérations sur le système en un seul objet (on peut notamment parler du problème du lecture/écriture d’un fichier par plusieurs threads dans lequel on doit mettre une politique d’accès restreint pour qu’il n’y est pas d’ambiguïté). La première solution, la plus obvie, c’est de créer l’objet dans la fonction main et de ce dire, bon maintenant, je n’instancie plus ma classe dans un objet. Un premier problème ce pose, c’est que vous devrez toujours spécifier cette objet dans les fonctions qui l’utilisent entant que argument pour quelles puissent y avoir accès sans pour autant faire une nouvelle instanciation. Pour résoudre ce problème, il suffit de définir votre objet comme variable globale, ainsi, elle sera accessible de n’importe où (attention tout de même à cette utilisation). Néanmoins, il y a encore un problème. En effet, il peut arriver qu’un des développeurs oubli la restriction de ne pas instancier une nouvelle fois votre classe … Et là boum, comment qu’on fait ? On lui gueule dessus. Vous savez très bien que cette solution n’est pas bonne. Non, il faut restreindre, dans le fonctionnement de votre classe, l’instanciation multiple.

Les cas où on l’utilise sont diverses. On peut par exemple imaginer un objet représentant un composant matériel de votre ordinateur qui doit être restreint à une unique instanciation pour éviter des conflits de gestions des ressources du composant. Après, les cas varient en fonction de votre projet, là où il faut comprendre, c’est qu’à l’aide du Singleton vous allez pouvoir (vous ?) limiter à n’avoir qu’un objet de votre classe dans tout votre programme et qu’il soit accessible partout !

De base, il faut comprendre que le constructeur de notre classe doit être privé sinon, on pourra instancier votre classe n’importe quand. Mais alors … Comment on pourra instancier notre classe si son constructeur est privé ? On va définir une fonction statique dans votre classe qui va permettre d’instancier votre classe si, et seulement si l’instanciation n’a jamais été fait ! Maintenant, comment savoir si l’instanciation n’a jamais été fait ? On va créer un pointeur statique qui va pointé sur NULL si votre classe n’a jamais été instancié (c’est la valeur par défaut de notre pointeur) et dès qu’on fera appelle à notre méthode statique d’instanciation, notre pointeur pointera alors sur le nouvelle objet qu’on aura créé. Cela correspond donc à une condition qui vérifie si notre pointeur est NULL, si c’est le cas, alors on créer l’objet et on assigne son adresse à notre pointeur, sinon on retourne notre pointeur (car il ne peut que contenir notre objet) ce qui assure l’unique instanciation de notre objet. Et puisqu’on a défini notre méthode d’instanciation (qui fait tout simplement appelle au constructeur, hein) et notre pointeur comme étant statique, ils seront accessible partout dans notre programme tant qu’on a inclue le *.hpp de notre classe.

L’implémentation d’un Singleton ce fait simplement en définissant le constructeur et le destructeur entant que private et on créer une méthode qu’on nomme en général getInstance qui vérifiera si on a déjà créé l’objet ou pas. Dans un des cas, on crée l’objet et on assigne son adresse à notre pointeur statique, dans l’autre on retourne tout simplement le pointeur:

class Singleton
{
    private:
        Singleton();
        ~Singleton();

        static Singleton    *instance;
    public:
        static Singleton* getInstance() {
            if(instance == NULL)
                instance = new Singleton();

            return instance;
        }
        static void kill() {
            if(instance != NULL) {
                delete instance;
                instance = NULL;
            }
        }
};

// Dans le *.cpp
Singleton *Singleton::instance = NULL;

Si vous avez compris les paragraphes ci-dessus, vous pouvez comprendre ce code. Dans ce cas, on opère à chaque fois à une condition dès qu’il s’agira d’instancier notre Singleton avec la méthode getInstance. Comme on prend soin de définir notre pointeur instance comme NULL dans notre *.cpp, on a donc dès le départ aucun objet du type Singleton. Ensuite, dès qu’on fera appelle pour la première fois à notre méthode statique (c’est à dire de cette manière: Singleton *mon_singleton = Singleton::getInstance();), on créera notre objet Singleton à l’aide de l’opérateur new. Ensuite, si on refait appelle à notre méthode getInstance, on nous retournera notre pointeur Singleton::instance. Ainsi, la possibilité de créer un nouvelle objet Singleton est restreint (mais pas impossible, nous allons voir pourquoi). Puisqu’on définit le destructeur de notre classe comme private, il nous faut aussi une fonction membre qui détruise notre objet. C’est à cela que sert la fonction kill(). N’oubliez pas que new permet d’allouer la mémoire, il est donc obligatoire de devoir utiliser delete par la suite, sinon, il y a une fuite de mémoire. Bon, maintenant regardons le code plus en profondeur. On sait pourquoi le constructeur est private: c’est pour empêcher que le développeur instancie une nouvelle fois notre classe. De ce fait, on est dans l’obligation de créer une méthode statique (donc indépendante de l’objet) qui puisse créer une instance de notre classe. Celle ci fait un contrôle sur l’état de notre pointeur et juge si il est nécessaire d’instancier (si le pointeur est égal à NULL) ou pas selon le cas. Mais nous avons aussi notre destructeur qui est private, mais pourquoi ? Pour bloquer la copie. En effet, le compilateur s’amuse parfois à créer des méthodes pour vous. Il y a entre autre le constructeur de copie et l’opérateur =. On peut faire le test (avec g++) en créant une propriété dans notre classe Singleton (ainsi qu’une fonction modifier pour modifier la valeur de notre propriété et une fonction friend pour l’afficher avec l’opérateur <<), en spécifiant le destructeur comme public, et en tapant ceci dans le main:

Singleton*  t = Singleton::getInstance("Dino");
// Le constructeur reçoit comme argument
// la futur valeur de notre propriété

Singleton   e = (*t);
// On fait appelle au constructeur de copie
// sans l'avoir explicitement spécifié dans le code

e.set("Jack");

display((*t));

Singleton::kill();
// On supprime la supposée unique instance de Singleton

display(e);

On peut voir apparaître Hello Dino (ce qui correspond à notre objet pointé par t) et Hello Jack (ce qui correspond à e). Ainsi, ceci prouve qu’il existe 2 objet Singleton (ceci ce prouve aussi en faisant appelle à Singleton::kill() et en affichant e juste après). Notre variable e est donc un objet indépendant de ce que peut pointé notre pointeur t. L’objectif de notre Singleton n’y est pas d’où la nécessité de devoir mettre le destructeur en private. Dans ce cas ci, en spécifiant le destructeur comme private, vous aurez une erreur de compilation car créer un objet suppose que le compilateur soit capable de le supprimer aussi. Or, si le destructeur est private, le compilateur ne peut être capable de détruire le nouvelle objet, donc il y a une erreur dans la compilation. En effet, pour ce qui ce passe avec e, c’est tout simplement l’utilisation de l’opérateur =. Notre compilateur va donc, de lui même, surcharger cette opérateur pour ensuite renvoyer un nouvelle objet Singleton. Néanmoins, la création d’un objet suppose aussi sa destruction, or dans notre première implémentation (en spécifiant le destructeur comme private), il est impossible pour le compilateur de détruire l’objet e, donc il nous retourne une erreur (dans le cas ci-dessus, il nous retourne pas une erreur car on a bien pris soin de spécifier le destructeur comme public). Définir le destructeur comme private est donc essentiel néanmoins cette approche n’est pas spontané. On pourrait très bien définir l’opérateur = et le constructeur de copie comme private (on peut aussi créer de l’objet e de cette façon: Singleton e((*t);), cela reviendrait au même et ce serait plus évident dans la lecture de votre code. Après, c’est à vous de voir. Et bien entendu, puisque le destructeur est private, on est dans l’obligation de spécifier une fonction membre statique kill() pour supprimer notre objet (ceci peut être notamment source d’erreur car on est dans l’obligation de faire appelle à cette fonction mais personne n’est à l’abri d’un oubli).

Mais allons plus loin et bien qu’on est vue les grandes lignes du Singleton, il y a certaines choses qu’on peut optimiser. Il est possible que l’utilité du mot clé new n’y soit pas dans vos applications et que vous voudriez optimiser votre code pour justement ne pas dépendre de cette dernière fonction membre kill() qui est obligatoire. La possibilité est donc de créer l’objet sans le mot clé new. Cela ce passe notamment dans le *.cpp où, au lieu de faire appelle au mot clé new, on définira instance tout simplement comme une variable:

Singleton Singleton::instance;
// Tout comme: int   number;

Bien entendu, le type de instance doit être static Singleton instance (ce n’est plus un pointeur). Néanmoins, cela engendre différent problème et en particulier dans son utilisation car, pour garder la même politique (notamment en définissant le destructeur comme private), la possibilité d’assigner notre objet Singleton à une variable nous est impossible (car l’utilisation du constructeur de copie ou de l’opérateur = nous est interdite). Si vous tapez ce code:

Singleton   t = Singleton::getInstance();

Le compilateur va vous retourner une erreur comme quoi le destructeur est en private. L’instanciation de l’objet nous est donc impossible. Il y aurait un moyen avec les pointeurs mais ce serait des lignes et du temps perdu pour pas grand chose. L’utilisation de notre objet ce fera alors à l’aide de la transparence référentielle. En effet, nous allons utiliser notre objet de cette façon Singleton::getInstance().set("Jack") (pour l’utilisation de la fonction membre set()). Comme vous pouvez le constater, ce n’est pas très pratique, mais l’objet ce fera bien supprimer à la fin sans pour autant utiliser une fonction tierce comme on l’a vue avec kill(). La définition de la classe change quelque peu aussi car on peut vite parvenir à des erreurs de compilation obscures que je ne pourrais détailler ici. Je vous donne donc la définition de la classe qui n’utilise pas les pointeurs:

class Singleton
{
    private:
        Singleton() { }
        ~Singleton() { }

        static Singleton    instance;
    public:
        static Singleton& getInstance() throw() { return instance; }
};

// Dans le *.cpp
Singleton Singleton::instance;

Comme on peut le remarquer, on définit explicitement le contenu du constructeur et du destructeur. Sinon, il y a lieu d’une erreur de compilation (après, je n’ai pas réellement chercher sur ce point si quelqu’un peut m’éclairer). Ensuite, on retourne une référence à notre variable instance d’où notre transparence référentielle. Ensuite, on définit instance dans le *.cpp tout simplement comme on l’aurait fait avec une variable. Donc ce genre d’utilisation, c’est à vous de voir et personnellement, ce genre d’implémentation est un peu bancal à mon goût même si dans notre ancienne implémentation, on était obligé d’utiliser la fonction kill() (peut être parce que l’instanciation ce passe dans le *.cpp et pas dans le main). Néanmoins, cela présente l’avantage de ne pas devoir faire état d’une condition dans la fonction getInstance ce qui peut être un gain d’optimisation. Enfin, c’est à vous de décider. Maintenant, on a toute les données pour créer un unique objet dans notre code. Seulement, même si théoriquement, tout ce passe bien, dans le cadre d’une application avec des threads (pour notre première implémentation), on peut être face à un problème si 2 threads demandent un accès à notre Singleton, donc un accès concurrent (en vérité, le problème ce relate aussi avec l’accès à une fichier par 2 threads, c’est un accès concurrent et il faut gérer cette accès selon une file d’attente).

La solution serait d’utiliser les mutex qui permettent justement de faire face à ce genre de situation. Je vous invite d’ailleurs à lire mon article sur les threads (et les fork) à cette adresse. Mais après cela dépend de votre application et il est conseiller de rester très concentrer quant il s’agit de faire une application multithreadée avec les Singletons/fichiers.


Bon, on a vue les grandes lignes et il n’est pas dans votre intérêt de savoir par cœur l’implémentation d’un Singleton mais juste de savoir son fonctionnement et, au pire, écrire les bouts de codes sur un papier ou un cahier de notes. Par contre, il est essentiel de savoir qu’une tel stratégie dans l’architecture logistique existe car vous serez alors plus apte à créer des programmes et résoudre des problèmes en rapport avec l’architecture de votre logiciel pour des questions de maintenant et de partage de votre code.