Notre page Facebook | |
Notre groupe Facebook |
|_Accueil Langage C++
|_RAII et les « smart pointers »
|_Mise en oeuvre d'une classe de « smart pointers »
|_Utilisation de la classe std::unique_ptr
|_Utilisation de la classe std::shared_ptr
|_Gestion des cycles de « smart pointers »
|_Application de RAII à la gestion des Mutex
Accès rapide :
Définition de la classe générique de pointeurs malins
Codage de votre classe de ressources
Utilisation des pointeurs malins pour la gestion de vos ressources
Cas des « factories »
Constructeurs explicites
Comme expliqué dans la section précédente, nous allons maintenant chercher à coder notre propre classe de pointeurs malins (smart pointers). Nous sommes bien d'accord : normalement, nous ne devrions pas faire cela étant donné que le standard C++ 2011 fourni déjà plusieurs types d'implémentations de pointeurs malins. Nous allons néanmoins procéder à cet exercice uniquement dans un but purement pédagogique. Sur les pages suivantes, il sera bien entendu préférable d'utiliser les classes standards.
Une classe de pointeurs malins doit donc permettre de manipuler des objets alloués dynamiquement (via l'opérateur new
) de nature quelconque.
Il en découle qu'il est donc judicieux de passer par un template pour mettre en oeuvre une telle classe, afin de pouvoir profiter de la généricité.
Ensuite, une classe de pointeurs malins doit contenir un pointeur vers la ressource allouée dynamiquement. Elle devra de plus permettre de partager un compteur
de pointeurs sur la ressource et ce compteur sera partagé par tous les pointeurs malins liés à cette ressource (nous allons pouvoir dupliquer les pointeurs malins).
Notre classe template devra donc contenir deux attributs.
Ensuite, deux comportements majeurs devront être gérés par votre classe de pointeurs malins :
Vos pointeurs malins doivent préciser ce qui se passe en cas de copie des instances de pointeurs malins. Dans notre cas, nous allons accepter la copie et gérer le compteur de pointeurs en conséquence. Il faut donc fournir un constructeur par copie et l'opérateur de copie. Bien entendu, il faudra aussi fournir un destructeur pour gérer la perte d'une copie de pointeur malin.
Vos pointeurs malins doivent aussi se comporter comme des pointeurs classiques, afin que la couche d'abstraction offerte par vos pointeurs malins soit
quasi neutre sur la manière de programmer des utilisateurs de votre classe. Cela implique que vous redéfinissiez à minima l'opérateur ->
et l'opérateur *
(pour récupérer la valeur derrière le pointeur).
Voici une proposition de codage pour une telle classe template.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
#ifndef __SHARED_PTR #define __SHARED_PTR /////////////////////////////////////////////////////////// // Templated class declaration /////////////////////////////////////////////////////////// template <typename RESOURCE> class SharedPtr { RESOURCE * rawPointer; unsigned int * pCounter; void decrease(); public: inline SharedPtr(const RESOURCE * rawPointer = nullptr); inline SharedPtr(const SharedPtr<RESOURCE> & original); inline ~SharedPtr(); inline SharedPtr<RESOURCE> & operator=(const SharedPtr<RESOURCE> & original); inline void reset(); inline RESOURCE * get() const; inline RESOURCE * operator->() const; inline operator RESOURCE *() const; inline RESOURCE & operator*() const; }; /////////////////////////////////////////////////////////// // Method implementations /////////////////////////////////////////////////////////// template <typename RESOURCE> SharedPtr<RESOURCE>::SharedPtr(const RESOURCE * rawPointer) : rawPointer(const_cast<RESOURCE *>(rawPointer)), pCounter(new unsigned int(1)) { } template <typename RESOURCE> SharedPtr<RESOURCE>::SharedPtr(const SharedPtr<RESOURCE> & original) { this->rawPointer = original.rawPointer; this->pCounter = original.pCounter; (*pCounter)++; } template <typename RESOURCE> void SharedPtr<RESOURCE>::decrease() { if (--(*this->pCounter) == 0) { delete this->pCounter; delete this->rawPointer; } } template <typename RESOURCE> SharedPtr<RESOURCE>::~SharedPtr() { this->decrease(); } template <typename RESOURCE> SharedPtr<RESOURCE> & SharedPtr<RESOURCE>::operator=(const SharedPtr<RESOURCE> & original) { if (this->rawPointer != original.rawPointer) { this->decrease(); this->rawPointer = original.rawPointer; this->pCounter = original.pCounter; (*pCounter)++; } return *this; } template <typename RESOURCE> void SharedPtr<RESOURCE>::reset() { this->decrease(); this->rawPointer = nullptr; (*this->pCounter) = 1; } template <typename RESOURCE> RESOURCE * SharedPtr<RESOURCE>::operator ->() const { return this->rawPointer; } template <typename RESOURCE> RESOURCE * SharedPtr<RESOURCE>::get() const { return this->rawPointer; } template <typename RESOURCE> SharedPtr<RESOURCE>::operator RESOURCE *() const { return this->rawPointer; } template <typename RESOURCE> RESOURCE & SharedPtr<RESOURCE>::operator *() const { return *this->rawPointer; } #endif |
Afin de comprendre l'exécution de notre programme nous allons créer une classe dont les instances seront prises en charge par nos pointeurs malins. Nous allons définir dans cette classe trois méthodes afin d'avoir un mini cycle de vie sur ces instances. A chaque point de ce cycle de vie, nous allons produire un affichage sur la console. Parmi ces trois méthodes nous aurons, bien entendu, le destructeur de notre classe et nous apporterons une attention toute particulière au déclenchement automatique de cette méthode. Voici une proposition de déclaration de cette classe.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#ifndef __RESOURCE #define __RESOURCE #include <string> #include "SharedPtr.h" class Resource { std::string name; public: Resource( const std::string & name ); ~Resource(); void doSomething(); }; typedef SharedPtr< Resource > ResourcePtr; #endif |
Et voici maintenant le fichier d'implémentation de notre classe.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <iostream> using namespace std; #include "Resource.h" Resource::Resource( const std::string & name ) : name( name ) { cout << "Resource " << name << " created" << endl; } Resource::~Resource() { cout << "Resource " << name << " released" << endl; } void Resource::doSomething() { cout << "Resource " << name << " used" << endl; } |
Notez la définition du typedef
afin de simplifier l'utilisation de nos pointeurs malins.
Nous sommes maintenant près à faire nos premiers tests sur l'utilisation de nos pointeurs malins. Voici un premier exemple basique.
Le pointeur malin est construit à partir du pointeur retourné par l'opérateur new
. Ensuite la perte de l'instance de pointeur malin entraine
la libération automatique de la ressource.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> using namespace std; #include "Resource.h" int main(int argc, char * argv[]) { ResourcePtr ptr1 = new Resource( "Demo" ); ptr1->doSomething(); return 0; } |
Compilons maintenant notre programme et lançons-le.
$> g++ -std=c++14 *.cpp -o raii $> raii Resource Demo created Resource Demo used Resource Demo released $>
Voici un exemple de fonction qu'on peut retrouver dans la librairie POSIX (Portable Operating System Interface X). Question : pourquoi ce type de fonction ne se retrouve pas dans la librairie standard C ?
1 2 3 4 5 |
char * strdup(const char * original) { size_t length = strlen(original) + 1; char * buffer = new char[length]; return strcpy(buffer, original); } |
La raison en est simple, c'est une histoire de responsabilité. Dans ce genre de fonction, la responsabilité d'allouer une ressource est donnée à la fonction alors
que la responsabilité de libérer la ressource est réservée à l'utilisateur de la fonction strdup
.
Résultat : souvent les développeurs ne sont pas conscients qu'ils ont cette responsabilité de libérer la ressource. En conséquence, certains développeurs C et C++
s'interdisent de coder ce type de fonctions/méthodes qui allouent des ressources.
Avec RAII, on peut se réconcilier avec ce genre de code, étant donné que la responsabilité de libérer la ressource n'est plus donnée à l'utilisateur de la librairie, mais, on contraire, exécutée automatiquement. On peut donc, par exemple, plus facilement mettre en oeuvre les designs patterns de construction (factory method, abstract factory, singleton, builder, ...).
Reprenons notre exemple précédent et testons ce type de fonctions : vous verrez qu'en fin d'exécution du programme, la ressource sera bien relâchée.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <iostream> using namespace std; #include "Resource.h" ResourcePtr aFactoryMethod() { ResourcePtr temp = new Resource( "Demo" ); temp->doSomething(); return temp; } int main(int argc, char * argv[]) { ResourcePtr ptr1 = aFactoryMethod(); ptr1->doSomething(); return 0; } |
Pour lancer ce programme :
$> g++ -std=c++14 *.cpp -o raii $> raii Resource Demo created Resource Demo used Resource Demo used Resource Demo released $>
Comme nous allons le voir dans les pages suivantes, des classes de pointeurs malins (avec diverses stratégies) sont déjà proposées en standards (du moins avec C++ ISO 2011). Une des différences entre notre classe et celles proposées en standard réside dans le fait que les constructeurs des classes standards seront marqués comme étant à invocation explicites. Modifions notre constructeur en conséquence pour se rapprocher des classes standards.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
#ifndef __SHARED_PTR #define __SHARED_PTR /////////////////////////////////////////////////////////// // Templated class declaration /////////////////////////////////////////////////////////// template <typename RESOURCE> class SharedPtr { RESOURCE * rawPointer; unsigned int * pCounter; void decrease(); public: explicit SharedPtr(const RESOURCE * rawPointer = nullptr); SharedPtr(const SharedPtr<RESOURCE> & original); ~SharedPtr(); SharedPtr<RESOURCE> & operator=(const SharedPtr<RESOURCE> & original); RESOURCE * get() const; RESOURCE * operator->() const; operator RESOURCE *() const; RESOURCE & operator*() const; }; /////////////////////////////////////////////////////////// // Method implementations /////////////////////////////////////////////////////////// // ... // Suite du code inchangé par rapport à l'exemple initial ... // ... #endif |
Et maintenant, revoici notre exemple d'utilisation d'un pointeur malin avec l'utilisation du constructeur à invocation explicite.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <iostream> using namespace std; #include "Resource.h" ResourcePtr aFactoryMethod() { ResourcePtr temp( new Resource( "Demo" ) ); // explicit constructor temp->doSomething(); return temp; } int main(int argc, char * argv[]) { ResourcePtr ptr1 = aFactoryMethod(); ptr1->doSomething(); return 0; } |
Si le principe est bien compris, nous pouvons maintenant passer à l'étude des classes standards C++ ISO 2011.
Améliorations / Corrections
Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.
Emplacement :
Description des améliorations :