Écrit par David Henry, le 28 juin 2003
Il est parfois nécessaire en programmation de créer un objet qui devra posséder durant tout le programme une instance unique : il peut s'agir par exemple d'un manager d'objet (texture manager, entity manager, ...), d'une fabrique de classe, ou bien de la classe encapsulant le moteur du jeu dont vous êtes en train de développer !
La solution (naïve) qui viendrait tout de suite à l'esprit pour cela serait de créer
un objet global et d'y accéder dans les différents fichiers sources du programme par le
biais du mot clé extern
, et ensuite « oublier » cette
classe :
extern UniqueObject *g_uniqueObject; // ... // Initialisation unique (début du programme) g_uniqueObject = new UniqueObject; g_uniqueObject->initialize (); // Utilisation de la classe unique g_uniqueObject->doSomething (); // Destruction unique à la fin du programme g_uniqueObject->shutdown (); delete g_uniqueObject;
Cette méthode pose plusieurs inconvénients. Rien n'empêcherait à un développeur (en supposant plusieurs programmeurs sur le projet) ne sachant pas qu'une instance globale de cet objet existait d'en créer une nouvelle. Les conséquences pourraient être graves : dans le cas par exemple d'un texture manager chargé de référencer chaque texture utilisée par le programme (ou le jeu) et de s'assurer de l'unicité de chacune d'elles (pas de texture en double dans la mémoire). Si plusieurs instances venaient à être créées, il se pourrait qu'une texture se trouve dans une de ces instances mais pas dans les autres, et qu'au besoin d'accéder à cette texture, l'instance ne la référençant pas soit incapable de la trouver bien qu'elle ait été chargée dans une autre instance de cette classe. Autre problème qui pourrait se poser : deux instances possédant la même texture !
Bref, tout ceci pourrait créer de lourds conflits à déboguer dans le programme. Il est donc nécessaire de trouver une parade à ce système. Une bonne solution consiste à empêcher la création de multiples objets d'une même classe et d'en assurer l'unicité durant tout le programme. Cette solution c'est le modèle Singleton.
Une classe singleton doit assurer deux fonctions : assurer une instance d'elle-même (qui sera unique) et empêcher le programmeur d'en créer d'autres.
Le problème est vite réglé : il suffit de déclarer le constructeur comme privé. Ainsi il sera impossible de créer une instance de la classe à l'extérieur de la classe elle-même. On pourra également déclarer le destructeur comme privé pour empêcher une destruction prématurée de l'instance unique par mégarde.
Pour cela, cette classe va posséder comme variable membre un pointeur vers une
instance d'elle-même. Pour que cette instance soit accessible tout au long du programme,
et donc pour qu'elle existe de sa création à la fin du programme, on va déclarer cet
objet comme static
.
Où créer cette instance ? Le constructeur étant privé, le seul endroit possible va être une fonction appartenant à cette classe. Où la détruire ? De la même manière, on ne va pouvoir la détruire qu'à l'intérieur d'une fonction de la classe.
Il faut également que cette instance soit accessible à n'importe quel moment du programme.
Une fonction statique rempliera cette tâche. De plus, c'est cette fonction qui va s'occuper
de créer l'instance unique si besoin et d'en renvoyer un pointeur. C'est à dire : à
l'appel de la fonction, l'instance unique sera créée si ce pointeur pointe sur
NULL
(ce qui devrait être le cas au début du programme) et retournera ensuite
un pointeur.
Voyons maintenant l'implémentation de cette classe :
///////////////////////////////////////////////////////////////////////////// // // UniqueObject - une classe à instance unique. // ///////////////////////////////////////////////////////////////////////////// class UniqueObject { private: // Constructeur/destructeur UniqueObject () : _value (0) { } ~UniqueObject () { } public: // Interface publique void setValue (int val) { _value = val; } int getValue () { return _value; } // Fonctions de création et destruction du singleton static UniqueObject *getInstance () { if (NULL == _singleton) { std::cout << "creating singleton." << std::endl; _singleton = new UniqueObject; } else { std::cout << "singleton already created!" << std::endl; } return _singleton; } static void kill () { if (NULL != _singleton) { delete _singleton; _singleton = NULL; } } private: // Variables membres int _value; static UniqueObject *_singleton; }; // Initialisation du singleton à NULL UniqueObject *UniqueObject::_singleton = NULL;
L'unique instance (_singleton
) étant statique, on doit l'initialiser dans
l'espace global du programme, ce qui nous arrange d'ailleurs car on peut ainsi au lancement
du programme initialiser le pointeur sur NULL
. On pourra par la suite créer
d'autres pointeurs que l'on initialisera grâce à la fonction getInstance()
.
Cette fonction s'assure bien ici de nous retourner un pointeur valide de cette instance
unique, puisque même si pas encore créée (première utilisation) ou bien déjà détruite
précédemment, un nouvel objet sera instancié. À sa destruction (fonction
kill()
), il ne faut pas oublier de faire pointer le singleton sur
NULL
pour une future utilisation possible de la classe.
Les fonctions setValue()
et getValue()
ainsi que la variable
membre _value
n'affectent en rien le modèle singleton, elles sont là pour
illustrer l'exemple suivant :
int main () { // pointeurs sur l'unique instance de la classe UniqueObject UniqueObject *obj1, *obj2; // initialisation des pointeurs obj1 = UniqueObject::getInstance (); obj2 = UniqueObject::getInstance (); // affectation de la valeur 11 à l'objet pointé par obj1 obj1->setValue (11); // affichage de _value std::cout << "obj1::_value = " << obj1->getValue () << std::endl; std::cout << "obj2::_value = " << obj2->getValue () << std::endl; // destruction de l'instance unique obj1->kill (); return 0; }
Voici ce que l'on obtient à l'exécution :
On remarque ici que obj1
et obj2
pointent sur le même objet,
puisqu'ils affichent la même valeur alors que seul obj1
a été affecté
de la valeur 11 (la valeur par défaut est 0 — voir constructeur).
Pour obtenir un pointeur sur cette instance de classe unique, on peut appeler
UniqueObject::getInstance()
de cette manière car cette classe est statique. De
même pour kill()
, on pourrait appeler à la place
UniqueObject::kill()
au lieu de passer par un des pointeurs.
Si vous avez plusieurs classes dont une seule instance doit exister dans tout le programme, l'implémentation du modèle singleton risque de devenir assez vite un peu lourde... D'autant plus qu'avec C++, on dispose de puissants et nombreux moyens à disposition pour récrire le moins de code possible ! Voyons donc une nouvelle approche à l'aide de l'héritage et des templates.
Pour commencer, nous allons isoler le modèle singleton en une classe de base, et
dériver ensuite toute classe à instance unique de cette classe singleton. Nous allons
ensuite avoir besoin d'utiliser les templates pour spécifier le type de classe
que getInstance()
devra construire. Voici une implémentation de la classe
template de base Singleton
:
///////////////////////////////////////////////////////////////////////////// // // Singleton - modèle Singleton applicable à n'importe quelle classe. // ///////////////////////////////////////////////////////////////////////////// template <typename T> class Singleton { protected: // Constructeur/destructeur Singleton () { } ~Singleton () { std::cout << "destroying singleton." << std::endl; } public: // Interface publique static T *getInstance () { if (NULL == _singleton) { std::cout << "creating singleton." << std::endl; _singleton = new T; } else { std::cout << "singleton already created!" << std::endl; } return (static_cast<T*> (_singleton)); } static void kill () { if (NULL != _singleton) { delete _singleton; _singleton = NULL; } } private: // Unique instance static T *_singleton; }; template <typename T> T *Singleton<T>::_singleton = NULL;
Rien de bien nouveau par rapport à la méthode précédente, mis à part l'arrivée d'une
classe template et l'isolation des fonctions appartenant uniquement au modèle
singleton. À noter : les constructeur et destructeur ont maintenant le statut
protected, pour permettre aux futures classes dérivées d'avoir accès aux
constructeurs et destructeurs de leur classe de base. Voici maintenant un exemple de classe
à instance unique, dérivée de Singleton
:
///////////////////////////////////////////////////////////////////////////// // // UniqueObject - une classe à instance unique. // ///////////////////////////////////////////////////////////////////////////// class UniqueObject : public Singleton<UniqueObject> { friend class Singleton<UniqueObject>; private: // Constructeur/destructeur UniqueObject () : _value (0) { } ~UniqueObject () { } public: // Interface publique void setValue (int val) { _value = val; } int getValue () { return _value; } private: // Variable membre int _value; };
À présent, la classe UniqueObject
est dérivée de Singleton
. Le
paramètre T
du template est la classe dont le singleton aura pour tâche de
gérer une unique instance de type UniqueObject
.
Il est également nécessaire de spécifier la classe
Singleton<UniqueObject>
(c'est à dire la classe singleton de base
spécifique à UniqueObject
) comme classe amie (friend
), lui
permettant ainsi à elle et à elle seule, l'accès aux constructeur et destructeur de
UniqueObject
.
Il est temps maintenant de voir un exemple d'utilisation de cette dernière implémentation du modèle singleton :
int main () { // pointeurs sur l'unique instance de la classe UniqueObject UniqueObject *obj1, *obj2, *obj3; // initialisation des pointeurs obj1 = UniqueObject::getInstance (); obj2 = UniqueObject::getInstance (); obj3 = UniqueObject::getInstance (); // affectation de la valeur 15 à l'objet pointé par obj1 obj1->setValue (15); // affichage de _value std::cout << "obj1::_value = " << obj1->getValue () << std::endl; std::cout << "obj2::_value = " << obj2->getValue () << std::endl; std::cout << "obj3::_value = " << obj3->getValue () << std::endl; // destruction de l'instance unique obj1->kill (); return 0; }
À un détail près, la fonction main()
est identique à la précédente.
Cette fois encore, on remarque que obj1
, obj2
et
obj3
pointent sur le même objet : l'instance unique de
UniqueObject
. Voici ce que l'on obtient à l'exécution :
Le modèle singleton permet une instanciation unique d'une classe de manière plus sûre que l'utilisation de variables globales. Il est d'ailleurs recommandé d'utiliser ce modèle pour des classes type « manager » (resource manager, etc.).
En modifiant un peu le code, on peut étendre ce modèle au « doubleton », « tripleton », etc., pour autoriser un nombre limité de classes de la même manière qu'ici.
Cet article est mis à disposition sous un contrat Creative Commons (licence CC-BY-ND).