Introduction

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 !

Une seule instance dans tout le programme

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.

Implémentation d'une classe 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.

Empêcher la création de multiples instances de la classe

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.

Assurer une instance de la classe

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.

Implémentation

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 :

résultat de l'exemple n°1

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.

Une meilleure implémentation grâce aux templates

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 :

résultat de l'exemple n°2

Conclusion

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.

Creative Commons Logo Contrat Creative Commons

Cet article est mis à disposition sous un contrat Creative Commons (licence CC-BY-ND).