Les smart pointers

Voir le sujet précédent Voir le sujet suivant Aller en bas

Les smart pointers

Message par Akabane87 le Dim 17 Mar - 15:13

Ce sujet explique le fonctionnement et l'utilité des smart pointers et implémente 2 classes de smart pointers : une que l'on peut trouver dans certaines libs c++ comme boost, et une autre de ma création permettant de faire des choses encore plus folles What a Face .

Ce tuto s’adresse à des personnes maîtrisant bien le C++ et surtout les pointeurs et les allocations dynamiques. Donc si vous n'êtes pas dans ce cas, passez votre chemins si vous voulez conserver vos neurones Razz .


Tout d'abord commençons par l'utilité des smart pointers. Vous avez sûrement déjà été dans le cas où votre programme crash à cause d'un double delete ou à l'inverse une fuite de mémoire qui devient soudainement insolvable car l'endroit dans le code où le delete devrait s'exécuter dépend de l'utilisation de votre programme. Il s'agit bien sûr du cas où une zone mémoire allouée dynamiquement est pointée par plusieurs pointeurs et où selon l’utilisation de votre programme, le delete devrait se passer sur un pointeur plutôt que l'autre ou inversement. Vous vous dites alors "bordel j'aimerais bien que mes autres pointeurs passent à NULL" une fois la mémoire désallouée ou bien "Zut ça serait cool si je savais si d'autres pointeurs pointent dessus pour savoir si je dois désallouer chez moi ou chez eux". Selon l'architecture de votre programme, il est soit difficile soit impossible de réaliser cela et vous vous dites que votre code est bancal Evil or Very Mad .

Et bien en fait non il n'est pas bancal, c'est juste que vous venez d'atteindre la limite de l'utilisation des pointeurs et que dans ce cas particulier il vous faudrait des pointeurs "intelligents" ; pas parce que vous êtes bête, mais parce que vous ne pouvez pas deviner comment votre programme va se comporter et gérer en conséquent la mémoire vous même. Et pour rendre le pointeur intelligent il existe le smart pointer et plus précisément le shared pointer (pointeur partagé) clown .

Son fonctionnement est simple : il s'agit d'un container qui va conserver un pointeur sur un objet que vous allez allouer et va l'associer à un compteur de références. Vous pourrez alors déclarer d'autres shared pointeurs pointant sur le même objet qui iront alors incrémenter ce compteur. Lorsque vous mettez à NULL ou changez la valeur de votre shared pointeur (pour pointer sur un autre objet) le compteur sera décrémenté. Et lorsqu'il atteint zero le shared pointeur désalloue la mémoire comme un grand, détruisant ainsi votre objet lorsque le dernier pointeur pointant la zone mémoire est relâché.

Je fais ici une petite parenthèse bas niveau sur la mémoire de la stack et de la heap afin d'expliquer comment marche vraiment le smart pointer. La grosse différence entre le smart pointer et le pointeur standard est que lorsque vous allouez un pointeur normal avec MonType* monPointeur = new MonType; , vous allouez une zone mémoire de la taille de MonType sur la heap, l'initialisez par le constructeur de MonType, et mettez sur la stack l'adresse de cette zone mémoire. Si monPointeur est déclaré dans une fonction, l'adresse de ce pointeur sera "perdue" à la fin de la fonction ou du scope si personne d'autre ne pointe dessus, causant alors une fuite mémoire car la zone mémoire n'aura pas été désallouée. Il s'agit là d'un phénomène bien appréhendé par le codeur confirmé mais qui pose problème dans l'exemple évoqué plus haut où l'on voudrait que le dernier utilisateur de la zone mémoire désalloue lui même la mémoire lorsqu'il ne l'utilise plus : on ne peut parfois pas savoir qui sera le dernier utilisateur Suspect . Et bien le smart pointeur permet de gérer justement ce problème de déréférencement de l'objet. Imaginons le code suivant : MonSmartPointerDeMonType Sp(new MonType); . On crée un smart pointer de MonType qu'on initialise avec l'adresse mémoire de notre objet alloué sur la heap et on met sur la stack un objet de type MonSmartPointerDeMonType. On a donc sur la stack un objet contenant une adresse et un compteur de références à 1. Si on atteint la fin du scope ou de la fonction qui déclare ce smart pointer on va détruire cet objet sur la stack et donc passer dans le destructeur de MonSmartPointerDeMonType, qui, oh miracle, désalloue la mémoire si le compteur de références (décrémenté dans le destructeur vaut 0). Si jamais on a entre temps créé ailleurs un autre smart pointeur et qu'on lui a donné par copie notre smart pointer pour le construire, on aura alors un compteur de référence à 2 et la mémoire ne sera pas désalloué lorsque notre 1er smart pointer sera détruit.

Le smart pointer consiste en fait simplement à créer un container destiné à détruire par son destructeur la zone mémoire automatiquement selon le besoin de son utilisateur sans qu'il aie à se demander s'il est vraiment le dernier utilisateur de la zone mémoire pointée (grâce au compteur de références). C'est extrêmement utile si vous avez des ressources dans un jeu qui ont besoin d'être loadée et déloadées à la volée selon que quelqu'un les utilise ou non.

Passons maintenant à la manière d'implémenter ce smart pointer (j'ai utilisé un template pour qu'il soit utilisable avec tous les types d'objets) :
Code:

class ReferenceCounter
{
private:
   int m_Count; // Reference count

public:
   void Grab()
   {
      // Increment the reference count
      ++m_Count;
   }

   int Drop()
   {
      // Decrement the reference count and
      // return the reference count.
      return --m_Count;
   }

   int GetRefCount() const
   {
      return m_Count;
   }
};

template < typename T >
class SmartPointer
{
private:
   T*    m_Data;      // pointer
   ReferenceCounter* m_RefCounter; // Reference count

public:
   SmartPointer() : m_Data(0), m_RefCounter(0)
   {
      // Create a new reference
      m_RefCounter = new ReferenceCounter();
      // Increment the reference count
      m_RefCounter->Grab();
   }

   SmartPointer(T* pValue) : m_Data(pValue), m_RefCounter(0)
   {
      // Create a new reference
      m_RefCounter = new ReferenceCounter();
      // Increment the reference count
      m_RefCounter->Grab();
   }

   SmartPointer(const SmartPointer<T>& sp) : m_Data(sp.m_Data), m_RefCounter(sp.m_RefCounter)
   {
      // Copy constructor
      // Copy the data and reference pointer
      // and increment the reference count
      m_RefCounter->Grab();
   }

   ~SmartPointer()
   {
      // Destructor
      // Decrement the reference count
      // if reference become zero delete the data
      if(m_RefCounter->Drop() == 0)
      {
         delete m_Data;
         delete m_RefCounter;
      }
   }

   int GetRefCount() const
   {
      return m_RefCounter?m_RefCounter->GetRefCount():0;
   }

   bool IsValid() const
   {
      return (m_Data && m_RefCounter && m_RefCounter->GetRefCount() > 0);
   }

   T& operator* ()
   {
      return *m_Data;
   }

   T& operator* () const
   {
      return *m_Data;
   }

   T* operator-> ()
   {
      return m_Data;
   }

   T* operator-> () const
   {
      return m_Data;
   }

   SmartPointer<T>& operator = (const SmartPointer<T>& sp)
   {
      // Assignment operator
      if (this != &sp) // Avoid self assignment
      {
         // Decrement the old reference count
         // if reference become zero delete the old data
         if(m_RefCounter->Drop() == 0)
         {
            delete m_Data;
            delete m_RefCounter;
         }

         // Copy the data and reference pointer
         // and increment the reference count
         m_Data = sp.m_Data;
         m_RefCounter = sp.m_RefCounter;
         m_RefCounter->Grab();
      }
      return *this;
   }

   bool operator == (const SmartPointer<T>& sp) const
   {
      return (m_Data == sp.m_Data);
   }

   bool operator != (const SmartPointer<T>& sp) const
   {
      return (m_Data != sp.m_Data);
   }
};

La principale difficulté est de gérer la vie du compteur de référence. Il est en effet trivial de créer l'objet et son compteur de références : la mémoire est allouée par l'utilisateur lorsqu'il déclare son smart pointer avec SmartPointer sp = new MonType; , et le compteur de référence est lui aussi alloué sur la heap puis incrémenté. Si un 2ème smart pointeur est utilisé avec SmartPointer sp2 = sp; alors on récupère dans sp2 le pointeur de Montype alloué ainsi que le pointeur sur le compteur de références (normal il est sensé être commun à tous les utilisateurs de l'objet pour pouvoir fonctionner Laughing ). Quand le dernier smart pointer utilisant la mémoire est détruit, la mémoire de l'objet ET du compteur de références sont désallouées et tout se passe bien.

L'utilisation du smart pointeur par rapport à un pointeur standard est rendue identique grâce à la surcharge des opérateurs *, ->, ==, != et = . On peut peut faire sp->UnMembreDeMonType comme on aurait fait ptrDeMonType->UnMembreDeMonType. Pour tester son pointeur (des fois qu'on ait fait sp = NULL), on peut utiliser sp.IsValid();
Enfin pour optimiser un peu la classe de smart pointer, on peut inliner toutes les fonctions de la classe SmartPointer afin de s'épargner l'appel aux fonctions à chaque fois (ce que fait par exemple la lib boost).

Evidemment il ne faut pas non plus faire n'importe quoi avec les smart pointers. Par exemple faire :
Code:

MonType o;// alloué sur la stack et donc valide jusqu'à la fin du scope
SmartPointer<MonType> sp = &o;// INTERDIT !! On va stocker dans le smart pointer une adresse temporaire sur la stack xD

// ou

MonType* p = new MonType;
SmartPointer<MonType> sp1 = p;// OK
SmartPointer<MonType> sp2 = p;// pas OK : on a dit au 2ème smart pointer que l'on possède p à nous tout seul alors que ce n'est pas vrai car sp1 pense la même chose. Du coup double delete assuré quand sp1 et sp2 seront détruits


Ces 2 cas ci dessus sont strictement interdits. On utilise l'opérateur = avec un membre de droite (ou le constructeur avec un membre) de type pointeur SEULEMENT SI on fait un new exclusif à ce smart pointer. Sinon on utilise l'opérateur = avec un membre de droite de type smart pointer ou le constructeur par copie.


Maintenant pour aller un peu plus loin on va voir comment gérer les références faibles et faire en sorte que notre smart pointeur soit capable de se comporter aussi en observateur (pas influencer le compteur de références et passer automatiquement à NULL lorsque plus personne ne "possède" l'objet pointé.

Voici le code : gare à la surtension de neurones Rolling Eyes :
Code:

template < typename T >
class SharedPointer
{
private:
   T**    m_Data;      // pointer of pointer
   ReferenceCounter* m_RefDataCounter; // Reference count for data
   ReferenceCounter* m_RefCounter; // Reference count for pointer
   bool m_IsOwner;// can't become owner if non initialized with ownership

public:
   SharedPointer(T* pValue = NULL, bool isOwner = true) : m_Data(NULL), m_RefCounter(NULL), m_RefDataCounter(NULL), m_IsOwner(isOwner)
   {
      m_Data = new T*(pValue);// make our pointer of data pointer point on the value's pointer

      if(isOwner)
      {
         // Create a new reference for the value
         m_RefDataCounter = new ReferenceCounter();
         // Increment the reference count
         m_RefDataCounter->Grab();
      }

      // Create a new reference for our pointer of pointer
      m_RefCounter = new ReferenceCounter();
      // Increment the reference count
      m_RefCounter->Grab();
   }

   SharedPointer(const SharedPointer<T>& sp, bool isOwner = true) : m_Data(sp.m_Data), m_RefCounter(sp.m_RefCounter), m_RefDataCounter(NULL), m_IsOwner(isOwner)
   {
      // Copy constructor
      // Copy the data and reference pointer
      // and increment the reference count
      m_RefCounter->Grab();

      if(isOwner)
      {
         m_RefDataCounter = sp.m_RefDataCounter;
         m_RefDataCounter->Grab();
      }
   }

   ~SharedPointer()
   {
      // Destructor
      // Decrement the reference count of the object if exists (else we are not the owner so we do nothing else for the pointer)
      // if reference become zero delete the data
      if(m_IsOwner && m_RefDataCounter && m_RefDataCounter->Drop() == 0)
      {
         assert(m_RefCounter->GetRefCount() > m_RefDataCounter->GetRefCount());// we can't have more references on the object than the refs on the pointer itself
         delete *m_Data;// delete the data
         delete m_RefDataCounter;
         *m_Data = NULL;// make it point on NULL for those who don't own it
         m_RefDataCounter = NULL;
      }
      
      // Decrement the reference count of the pointer
      // if reference become zero delete the pointer too
      if(m_RefCounter->Drop() == 0)
      {
         delete m_Data;// delete the pointer
         delete m_RefCounter;
         m_Data = NULL;// point NULL to be sure
         m_RefCounter = NULL;
      }
   }

   const bool GetIsOwner() const {return m_IsOwner;}
   void DropOwnership()
   {
      if(!m_IsOwner)
         return;

      if(m_RefDataCounter && m_RefDataCounter->Drop() == 0)// I was the last owner of this data and I want to give back ownership : delete all
      {
         delete *m_Data;// delete the data
         delete m_RefDataCounter;
         *m_Data = NULL;// make it point on NULL for those who don't own it
      }

      m_RefDataCounter = NULL;// in any case : drop the refdatacounter (deleted or not depending code above)

      m_IsOwner = false;
   }

   int GetRefCount() const
   {
      return m_RefCounter?m_RefCounter->GetRefCount():0;
   }
   int GetRefDataCount() const
   {
      return m_RefDataCounter?m_RefDataCounter->GetRefCount():0;
   }

   bool IsValid() const
   {
      return (*m_Data && m_RefCounter && m_RefCounter->GetRefCount() > 0);
   }

   T& operator* ()
   {
      return **m_Data;
   }

   T& operator* () const
   {
      return **m_Data;
   }

   T* operator-> ()
   {
      return *m_Data;
   }

   T* operator-> () const
   {
      return *m_Data;
   }

   SharedPointer<T>& operator = (const SharedPointer<T>& sp)
   {
      // Assignment operator
      if (this != &sp) // Avoid self assignment
      {
         // Decrement the reference count of the object if exists (else we are not the owner so we do nothing else for the pointer)
         // if reference become zero delete the data
         if(m_IsOwner && m_RefDataCounter && m_RefDataCounter->Drop() == 0)
         {
            assert(m_RefCounter->GetRefCount() > m_RefDataCounter->GetRefCount());// we can't have more references on the object than the refs on the pointer itself
            delete *m_Data;// delete the data
            delete m_RefDataCounter;
            *m_Data = NULL;// make it point on NULL for those who don't own it
            m_RefDataCounter = NULL;
         }
         assert(!m_RefDataCounter && !*m_Data);// I am not supposed to have any data here

         if(m_IsOwner)
         {
            delete m_Data;// delete the data pointer
            delete m_RefCounter;
            m_Data = NULL;
            m_RefCounter = NULL;
         }

         // Copy the data pointer and reference pointer
         // and increment the reference count
         m_Data = sp.m_Data;
         m_RefCounter = sp.m_RefCounter;
         m_RefCounter->Grab();
         if(m_IsOwner)
         {
            m_RefDataCounter = sp.m_RefDataCounter;
            m_RefDataCounter->Grab();
         }
      }
      return *this;
   }

   bool operator == (const SharedPointer<T>& sp) const
   {
      return (*m_Data == *(sp.m_Data));
   }

   bool operator != (const SharedPointer<T>& sp) const
   {
      return (*m_Data != *(sp.m_Data));
   }
};

Je ne vais pas analyser dans le détail le code car je risque de perdre le peu de lecteurs qu'il me reste Embarassed . Sachez seulement que l'on a remplacé par rapport à la classe d'avant le pointeur de type T par un pointeur de pointeur pour justement être capable de le faire pointer la même chose chez tout le monde. Ce pointeur de pointeur nécessitant une allocation supplémentaire (le pointeur de pointeur) indépendante de l'objet lui même si l'on veut gérer le fait d'être ou non owner de l'objet, il faut donc non plus 1 compteur de références mais 2 : un pour l'objet et un pour le pointeur.

L'utilisation de cette classe est la suivante :
Code:

{
SharedPointer<MonType> sp1 = new Montype; // ou SharedPointer<MonType> sp1(new Montype);
// sp1 est owner de l'objet
SharedPointer<MonType> sp2 = sp1; // ou SharedPointer<MonType> sp2(sp1);
// ou encore sp2 = NULL; puis sp2 = sp1; pour utiliser l'opérateur =
// sp2 est également owner de l'objet
static SharedPointer<MonType> sp3(sp1, false);
// sp3 n'est pas owner de l'objet ( je l'ai mis static afin de vous montrer que ça marche : le fait de le rendre static va faire en sorte qu'il ne sera pas détruit à la fin du scope et qu'il passera donc à NULL à la fin du scope (puisqu'il n'est pas owner)

// hop pour le fun on passe sp2 en non owner de l'objet juste avant le fin du scope
sp2.DropOwnership();
}// ici on détruit sp1 et sp2 car ils sont alloués sur la stack donc on détruit d'abord sp2 qui ne fait rien, puis sp1 qui étant le seul owner de l'objet va le détruire, faisant alors passer à NULL sp3


Voilà, vous savez désormais tout ce qu'il faut savoir sur les smart pointers. Beaucoup de gens ont tendance à ne plus utiliser que des smart pointeurs dans leur programmes. Moi je pense qu'il ne faut les utiliser que l'orsque l'on en a réellement besoin. une chose est cependant sûre, c'est qu'il ne faut pas mélanges les 2 pour un même objet car ça reviendrait à ne pas utiliser le smart pointeur du tout et à retomber dans les problèmes de fuite mémoire et double désallocation qu'on voulait justement éviter avec le smart pointer Shocked .
avatar
Akabane87
Admin

Messages : 45
Date d'inscription : 30/07/2010

Voir le profil de l'utilisateur http://dreamraiser.forumactif.com

Revenir en haut Aller en bas

Voir le sujet précédent Voir le sujet suivant Revenir en haut

- Sujets similaires

 
Permission de ce forum:
Vous ne pouvez pas répondre aux sujets dans ce forum