Pointeur intelligent ✱

Nous vous avons conseillé d’utiliser la classe vector de la STL pour gérer les allocations mémoire dynamique (principe RC0). Ainsi, lorsque vous voulez faire une liste d’objets de type A, aucun soucis, la classe vector convient et suffit amplement. Alors pourquoi parler des pointeurs ? A cause de l’héritage ! En effet, si vous voulez faire une liste d’objets appartenant à une même hiérarchie, alors le C++ ne saura pas gérer ! Prenons un exemple : supposons que nous ayons une classe mère A et deux classes filles B et C. Pour créer un container pouvant stocker des objets de type A, B ou C, il va falloir choisir un type comme argument du template vector. On choisira donc naturellement la classe mère de la hiérarchie et on déclarera un vector<A>. Malheureusement, en C++, cette syntaxe sous-entend que tous les objets dans ce container ne peuvent être QUE du type A. Impossible alors de stocker des objets de type B ou C. Pourquoi ? Rappelons que même si B et C sont des enfants de A, ils ne prennent pas forcément la même place en mémoire car ils peuvent stocker des informations supplémentaires. Or, un vector<A>, qui est en fait comme un tableau de A : A[], ne peut malheureusement stocker que des objets de même taille, d’où le problème. Pour sortir de l’impasse, on devra utiliser un vector de pointeurs vers des objets de type A et la mécanique interne du C++ va nous garantir qu’elle sera capable de déterminer le type exact : A, B ou C, de l’objet désigné par chaque pointeur. Bref, on ne peut pas enterrer l’utilisation des pointeurs dans le C++ moderne, sauf si vous n’utilisez pas l’héritage ! Cependant, dans un soucis de modernité, on va utiliser des smart pointeurs alliant la rapidité du C++ à la sécurité d’un garbage collector présent dans les autres langages comme Java ou C#.

Les shared_ptr

Présentation

Nous rappelons que :

Les mots-clefs new et delete SONT INTERDITS

Les fonctions malloc et free SONT INTERDITES

Si les opérations new/delete ne sont pas autorisées, alors comment que faire ? Nous allons vous présenter une nouvelle approche appelée pointeurs intelligents (smart pointers). Nous nous concentrons plus particulièrement sur les shared pointer qui allie l’efficacité des pointeurs avec la sécurité d’un garbage collector. En effet, un shared_ptr est un pointeur doublé d’un compteur d’utilisation vers l’entité désignée. Lorsque ce compteur atteint 0, alors le shared_ptr déclenche la destruction de l’objet comme le ferait un garbage collector.

Syntaxe

Pour instancier un objet et l’associer à un shared pointer, vous devez utiliser la syntaxe suivante :

shared_ptr<T> p = make_shared<T> (paramètres du constructeur);
ou
auto p = make_shared<T>(paramètres du constructeur);

Le mot clef auto permet de simplifier la syntaxe des instanciations. Pour accéder aux membres de l’objet associé au smart pointeur, il suffit d’utiliser l’opérateur -> classique des pointeurs. Voici un exemple ci-dessous :

#include <iostream>
#include <memory>
using namespace std;

struct Point
{
        int x, y;
        Point(int a, int b) : x(a), y(b) {  }
        void Aff() { cout << "(" << x << "," << y << ")"; }
};

int main()
{
        shared_ptr<Point> p1 = make_shared<Point>(1, 2);
        p1->Aff();

        auto p2 = make_shared<Point>(4, 5);
        p2->Aff();
}

>> (1,2)
>> (4,5)

Avertissement

Nous vous demandons d’utiliser les shared_ptr car ils constituent le bagage minimum d’un programmeur C++ moderne.

Est-ce que les shared_ptr résolvent tous les problèmes de libération de mémoire ? Malheureusement non. En effet, en cas de référence cyclique : un objet 1 pointant vers un objet 2 contenant lui-même un pointeur vers l’objet 1. Dans cette configuration, même si aucun pointeur ne référence ces deux objets, ils ne vont pas être détruits car les compteurs de références resteront bloqué à 1 à cause du cycle. Dans ce cas, il faudra utiliser un mécanisme supplémentaire pour sortir de l’impasse (hors programme). Nous vous conseillons, dans votre cas, d’éviter cette situation.

Exemple 1

Voici un exemple qui indique comment un shared_ptr se comporte lors des appels de fonction :

        #include <iostream>
        #include <memory>
        using namespace std;

        struct Point
        {
                int x, y;
                Point(int a, int b) : x(a), y(b) { cout << "Point créé\n"; }
                ~Point()                         { cout << "Point détruit\n"; }
                void Aff()                       { cout << "(" << x << "," << y << ")"<<endl; }
        };

        void test_1(shared_ptr<Point> & p1){   // passage par référence
5               cout << "test_1 compteur : " << p1.use_count() << "\n";   }

        void test_2(shared_ptr<Point> p2) {    // passage par copie
7               cout << "test_2 compteur : " << p2.use_count() << "\n";   }

        int main() {
1               auto p = make_shared<Point>(1, 2); // instanciation
2               p->Aff();
3               cout << "main compteur : " << p.use_count() << "\n";
4               test_1(p);
6               test_2(p);
8               cout << "main compteur : " << p.use_count() << "\n";
        }

1       >> Point créé
2       >> (1,2)
3       >> main compteur : 1
5       >> test_1 compteur : 1
7       >> test_2 compteur : 2
8       >> main compteur : 1
9       >> Point détruit

Description ligne à ligne :

  • 1 auto p = make_shared<Point>(1, 2) : crée un objet de type Point et un shared_ptr p pointant sur cet objet.

  • 2 p->Aff(); : affiche les coordonnées (x,y) du point en question.

  • 3 cout << « main compteur » : un seul shared_ptr point vers l’objet Point, le compteur est à 1.

  • 4 test_1(p) : appel de la fonction test_1, le shared_ptr est passé par référence.

  • 5 cout << « test_1 compteur », il n’y a pas eu de nouveau pointeur créé, le compteur est toujours égal à 1.

  • 6 test_2(p) : appel de la fonction test_2, le shared_ptr est passé par copie, un nouveau shared_ptr p2 désgine alors le même objet.

  • 7 cout << « test_2 compteur », les deux shared_ptr p et p2 pointent vers le même objet, le compteur vaut donc 2.

  • 8 cout << « main compteur », au retour dans la fonction main, le shared_ptr p2 a été détruit, le compteur retombe à 1.

  • 9 } fin de la fonction main(), le shared_ptr p est détruit, le compteur de tombe à 0, et l’objet Point est détruit automatiquement.

Exemple 2

Voici un exemple qui indique comment un shared_ptr se comporte avec des vector :

        #include <iostream>
        #include <memory>
        #include <vector>
        using namespace std;

        struct Point
        {
                int x, y;
                Point(int a, int b) : x(a), y(b) { cout << "Point créé    : "; Aff(); }
                ~Point()                         { cout << "Point détruit : "; Aff(); }
                void Aff()                       { cout << "(" << x << "," << y << ")"<<endl; }
        };

        int main()
        {
                vector<shared_ptr<Point>> L;

1               L.push_back( make_shared<Point>(1,2) );
1               L.push_back( make_shared<Point>(3,4) );
2               L.push_back(L[0]);

3               cout << "compteur L[0] : " << L[0].use_count() << "\n";
4               cout << "compteur L[1] : " << L[1].use_count() << "\n";

                cout << "L[1] détruit" << endl;
5               L.erase(L.begin()+1);

7               cout << "Fin du programme" << endl;
        }

        >> Point créé    : (1,2)
        >> Point créé    : (3,4)
        >> compteur L[0] : 2
        >> compteur L[1] : 1
        >> L[1] détruit
6       >> Point détruit : (3,4)
        >> Fin du programme
8       >> Point détruit : (1,2)

Commentaires :

  • 1 Deux objets Point sont créés et insérés dans un vector L.

  • 2 Le shared_ptr L[0] est copié pour créer un 3ème élément de L.

  • 3 Le compteur de L[0] vaut 2 car il y a deux shared_ptr pointant vers le même objet : L[0] et L[2].

  • 4 Le compteur de L[1] vaut 1 car il y a unique shared_ptr pointant vers cet objet.

  • 5 La ligne : L.erase(L.begin()+1) efface le shared_ptr L[1], le compteur tombe à 0.

  • 6 Par conséquent le Point : (3,4) est détruit dans la foulée : Point détruit : (3,4)

  • 7 La fonction main se termine, la liste L est détruite ainsi que les shared_ptr qu’elle contient.

  • 8 Le compteur de l’objet Point(1,2) tombe alors à 0 et cet objet est automatiquement détruit : Point détruit : (1,2).

Quizzz

  • Donnez la syntaxe pour créer un shared pointer sur un objet de type T construit avec les paramètres 3 et 5 (sans ;).

  • Depuis un shared pointer p, pour accéder au paramètre a, dois-je écrire p.a ?

  • Quelle est la valeur affichée par le code suivant :

    void test(shared_ptr<Point> p)  {  p->Aff();  }
    
    int main()
    {
            shared_ptr<Point> p = make_shared<Point>(1,2);
    
            for (int i = 0 ; i < 3 ; i++)   test(p);
    
            cout << p.use_count();
    
            return 0;
    }
    
  • Lorsque deux objets forment une dépendance cyclique, lorsqu’ils ne seront plus utilisés par le programme, l’utilisation de shared pointer permet de libérer ces objets contrairement aux pointeurs classiques.