Le Singleton

Écrit le 04/07/2003 par DukeNukem
Dernière mise à jour : 02/02/2006

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 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 CUniqueClasse *g_pUniqueObject;
// ...

// initialisation unique (début du programme)
g_pUniqueObject = new CUniqueClasse;
g_pUniqueObject->Initialize();

// utilisation de la classe unique
g_pUniqueObject->DoSomething();

// destruction unique à la fin du programme
g_pUniqueObject->Shutdown();
delete g_pUniqueObject;

Cependant 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 l'application graphique ou le jeu et de s'assurer de l'unicité de chacune (pas de texture chargée en double). 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 développeur 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.

Où créer cette instance ? Le constructeur étant privé, le seul endroit approprié 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 (ou 0, ce qui devrait être le cas au début du programme) et retournera ensuite un pointeur.

Voyons maintenant l'implémentation de cette classe :

// ==============================================
// CUniqueObject - classe à instance unique.
// ==============================================

class CUniqueObject
{
private:
    // constructeur/destructeur
    CUniqueObject( void ) : m_iValue(0) { }
    ~CUniqueObject( void ) { }


public:
    // fonctions publiques
    void    SetValue( int iValue ) { m_iValue = iValue; }
    int     GetValue( void ) { return m_iValue; }

    // fonctions de création et destruction du singleton
    static CUniqueObject *GetInstance( void )
    {
        if( m_pSingleton == 0 )
        {
            std::cout << "creating singleton." << std::endl;
            m_pSingleton =  new CUniqueObject;
        }
        else
        {
            std::cout << "singleton already created!" << std::endl;
        }

        return m_pSingleton;
    }

    static void Kill( void )
    {
        if( m_pSingleton != 0 )
        {
            delete m_pSingleton;
            m_pSingleton = 0;
        }
    }


private:
    // variables membres
    int                  m_iValue;
    static CUniqueObject *m_pSingleton;
};


// initialisation du singleton à 0
CUniqueObject *CUniqueObject::m_pSingleton = 0;

L'unique instance (*m_pSingleton) é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 0. 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étruire précédemment, un nouvel objet sera construit. À sa destruction (fonction Kill()), il ne faut pas oublier de faire pointer le singleton sur 0 pour une future utilisation possible de la classe.

Les fonctions SetValue() et GetValue() ainsi que la variable membre m_iValue n'affectent en rien le modèle singleton, elles sont là pour illustrer l'exemple suivant :

// ----------------------------------------------
// main() - fonction principale.
// ----------------------------------------------

int main( int argc, char *argv[] )
{
    // pointeurs sur l'unique instance de la classe CUniqueObject
    CUniqueObject *pObj1, *pObj2;

    // initialisation des pointeurs
    pObj1 = CUniqueObject::GetInstance();
    pObj2 = CUniqueObject::GetInstance();

    // affectation de la valeur 11 à l'objet pointé par pObj1
    pObj1->SetValue( 11 );

    // affichage de m_iValue
    std::cout << "pObj1::m_iValue = " << pObj1->GetValue() << std::endl;
    std::cout << "pObj2::m_iValue = " << pObj2->GetValue() << std::endl;

    // destruction de l'instance unique
    pObj1->Kill();

    return 0;
}

Voici ce que l'on obtient à l'exécution :

http://game-lab.com/images/tuts/c_singleton/singleton1.png

On remarque ici que *pObj1 et *pObj2 pointent sur le même objet, puisqu'ils affichent la même valeur alors que seul *pObj1 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 CUniqueObject::GetInstance() de cette manière car cette classe est statique. De même pour Kill(), on pourrait appeler à la place CUniqueObject::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 lourd... 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 CSingleton :

// ==============================================
// CSingleton - singleton.
// ==============================================

template <typename T> class CSingleton
{
protected:
    // constructeur/destructeur
    CSingleton( void ) { }
    ~CSingleton( void ) { std::cout << "destroying singleton." << std::endl; }


public:
    // fonctions publiques
    static T *GetInstance( void )
    {
        if( m_pSingleton == 0 )
        {
            std::cout << "creating singleton." << std::endl;
            m_pSingleton = new T;
        }
        else
        {
            std::cout << "singleton already created!" << std::endl;
        }

        return ((T *)m_pSingleton);
    }

    static void Kill( void )
    {
        if( m_pSingleton != 0 )
        {
            delete m_pSingleton;
            m_pSingleton = 0;
        }
    }


private:
    // variable membre privée
    static T    *m_pSingleton;

};


template <typename T> T *CSingleton::m_pSingleton = 0;

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 constructeur/destructeur de leur classe de base. Voici maintenant un exemple de classe à instance unique, dérivée de CSingleton :

// ==============================================
// CUniqueObject - classe à instance unique.
// ==============================================

class CUniqueObject : public CSingleton<CUniqueObject>
{
    friend class CSingleton<CUniqueObject>;

private:
    // constructeur/destructeur
    CUniqueObject( void ) : m_iValue(0) { }
    ~CUniqueObject( void ) { }


public:
    // fonctions publiques
    void    SetValue( int iValue ) { m_iValue = iValue; }
    int     GetValue( void ) { return m_iValue; }


private:
    // variable membre
    int     m_iValue;

};

À présent, la classe CUniqueObject est dérivée de CSingleton. Le paramètre T du template est la classe dont le singleton aura pour tâche de gérer une unique instance : CUniqueObject.

Il est également nécessaire de spécifier la classe CSingleton<CUniqueObject> (c'est à dire la classe singleton de base spécifique à CUniqueObject) comme classe amie (friend), lui permettant ainsi à elle et à elle seule, l'accès aux constructeur et destructeur de CUniqueObject.

Il est temps maintenant de voir un exemple d'utilisation de cette dernière implémentation du modèle singleton :

// ----------------------------------------------
// main() - fonction principale.
// ----------------------------------------------

int main( int argc, char *argv[] )
{
    // pointeurs sur l'unique instance de la classe CUniqueObject
    CUniqueObject *pObj1, *pObj2, *pObj3;

    // initialisation des pointeurs
    pObj1 = CUniqueObject::GetInstance();
    pObj2 = CUniqueObject::GetInstance();
    pObj3 = CUniqueObject::GetInstance();

    // affectation de la valeur 15 à l'objet pointé par pObj1
    pObj1->SetValue( 15 );

    // affichage de m_iValue
    std::cout << "pObj1::m_iValue = " << pObj1->GetValue() << std::endl;
    std::cout << "pObj2::m_iValue = " << pObj2->GetValue() << std::endl;
    std::cout << "pObj3::m_iValue = " << pObj3->GetValue() << std::endl;

    // destruction de l'instance unique
    pObj1->Kill();

    return 0;
}

À un détail près, la fonction main() est identique à la précédente. Cette fois encore, on remarque que *pObj1, *pObj2 et *pObj3 pointent sur le même objet : l'instance unique de CUniqueObject. Voici ce que l'on obtient à l'exécution :

http://game-lab.com/images/tuts/c_singleton/singleton2.png

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 ».

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.

Le code source des exemples de cet article sont réutilisables librement sans conditions particulières.

Creative Commons Logo Contrat Creative Commons

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

http://tfc.duke.free.fr/coding/singleton.html -->