Smart pointers


To translate this course in your native langage:

  • In Chrome: right-click anywhere on the page and select “Translate to…”

  • Or use this language selector:

Les mots colorés en anglais représentent des mots-clés de la norme du langage C++. Ils sont conservés à l’identique lors de l’utilisation d’une traduction automatique avec Google Translate. Exemple : l’héritage (inheritance) est un principe fondamental de C++.


Introduction

Le C++ moderne cherche autant que possible à éviter la manipulation directe des pointeurs. Les références, les conteneurs de la STL et de nombreux autres mécanismes permettent aujourd’hui d’écrire du code sans manipuler explicitement des adresses mémoire.

Cependant, les allocations dynamiques restent nécessaires dans de nombreuses situations. Lors du chargement d’un objet 3D, par exemple, il peut être nécessaire de créer dynamiquement différentes ressources : textures, géométrie, animations, matériaux, etc..

Nous choisissons de ne pas travailler avec l’opérateur d’allocation dynamique du C++ (new) car il est à l’origine de nombreuses fuite mémoire car il arrive que l’on oublie de libérer un objet.

Pour s’assurer que les objets seront correctement libérées lorsqu’ils ne sont plus utilisés, le C++ a mis en place des smart pointers qui allient l’efficacité des pointeurs à la sécurité d’un garbage collector. Nous allons nous intéresser plus particulièrement aux shared pointers qui permettent de partager une ressource entre plusieurs entités.

Shared pointers

Principe

Les shared_ptr sont utilisés lorsqu’un objet est partagé par plusieurs propriétaires. Ainsi chaque propriétaire dispose d’un shared_ptr lui permettant d’accéder à cet objet. En parallèle est maintenu un compteur unique indiquant le nombre de propriétaires faisant référence à cet objet. Lorsque le dernier shared_ptr disparaît, le compteur atteint zéro et l’objet est automatiquement détruit.

Syntaxe

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

shared_ptr<T> p = make_shared<T>(constructor parameters);
ou
auto p = make_shared<T>(constructor parameters);

Le mot clef auto permet de simplifier la syntaxe des instanciations.

Opérateur flèche

Pour accéder aux membres d’un objet associé à un smart pointeur, il faut utiliser l’opérateur flèche -> i.e. la notation 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()
{
        auto p1 = make_shared<Point>(4, 5);
        p1->Aff();
}

>> (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. Prenons un exemple :

  • Un objet A possède un shared_ptr vers un objet B

  • Un objet B possède un shared_ptr vers un objet A

Nous sommes face à une dépendance cyclique et dans ce cas, le mécanisme des shared_ptr reste impuissant. En effet, même si aucune association existe dans le programme vers l’objet A et l’objet B, chaque compteur d’utilisation reste bloqué à 1 à cause du cycle. Ainsi, aucun de ces deux objets ne pourra être libéré. Nous vous conseillons d’éviter cette situation dès la conception.

Comptage

Présentation

Que se passe-t-il lorsque plusieurs shared_ptr sont associés au même objet ? Regardons comment le compteur de références évolue :

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

struct A {};


int main()
{
        auto p1 = make_shared<A>(); // instantiation

        cout << "number of shared ptr : " << p1.use_count() << endl;

        auto p2 = p1;   // new shared ptr

        cout << "number of shared ptr : " << p1.use_count() << endl;
        cout << "number of shared ptr : " << p2.use_count() << endl;
}

>> 1
>> 2
>> 2

Description ligne à ligne :

  • auto p1 = make_shared<A>() : crée un objet de type A et un shared_ptr associé à cet objet.

  • auto p2 = p1; : crée un nouveau shared_ptr associé au même objet.

  • p1.use_count(): le compteur est égal à 2 car 2 shared_ptr différents sont associés au même objet.

  • p2.use_count(): le compteur vaut aussi 2 car ces deux shared_ptr sont liés au même objet.

Quizzz

  • Donnez la syntaxe pour créer un shared pointer sur un objet de type T. Ne pas écrire le ;

  • Donnez la syntaxe pour accéder au paramètre a depuis un shared pointer p.

  • 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;
    }
    
  • Un shared_ptr permet de libérer l’objet pointé dès que son compteur d’utilisation vaut -1.