Blog ENI : Toute la veille numérique !
🐠 -25€ dès 75€ 
+ 7 jours d'accès à la Bibliothèque Numérique ENI. Cliquez ici
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici
  1. Livres et vidéos
  2. Langage C++
  3. Pointeurs
Extrait - Langage C++ De l'héritage C au C++ moderne (avec programmes d'illustration) (2e édition)
Extraits du livre
Langage C++ De l'héritage C au C++ moderne (avec programmes d'illustration) (2e édition) Revenir à la page d'achat du livre

Pointeurs-objets ou "smart pointers"

Introduction

1. La bibliothèque memory

Depuis son apparition avec C++11, la bibliothèque <memory> fait l’objet d’un développement quasi continu. Elle appartient à l’espace de noms std où sont regroupées les bibliothèques standards. Son but est de sécuriser et d’optimiser la gestion des ressources dynamiques avec notamment un contrôle renforcé de la durée de vie des objets alloués dynamiquement. Elle se compose de plusieurs classes complémentaires entre elles et d’un ensemble de fonctions à part.

Parmi les classes proposées, trois déterminent des pointeurs constitués en objets et dénommés smart pointers : std::unique_ptr, std::shared_ptr et std::weak_ptr.

À ces trois classes s’ajoute une classe pour un destructeur par défaut, la classe default_delete qui encapsule sous forme d’objets les instructions delete et delete[ ]. Également, des objets shared_ptr s’utilisent parfois avec des allocateurs de mémoire obtenus avec la classe std::allocator.

Ce chapitre non exhaustif présente toutefois des fonctionnalités primordiales des pointeurs intelligents ou smart pointers. Ces objets-pointeurs constituent en eux-mêmes une très intéressante illustration des apports possibles de la conception et de la programmation objet.

2. Transformer un pointeur en objet

Un des objectifs des pointeurs intelligents consiste à lever l’obligation faite au programmeur de lui-même désallouer correctement et au bon moment la mémoire obtenue avec new ou new[]. Pour ce faire, le pointeur est tout simplement encapsulé dans un objet et l’adresse mémoire qu’il détient, si différente de null, est automatiquement libérée à la disparition...

La classe std::unique_ptr

Comme son nom l’indique, un pointeur unique_ptr est unique, c’est-à-dire qu’il ne peut pas partager l’adresse mémoire qu’il possède : sa valeur ne peut pas être copiée. Pour cette raison, il n’est pas utilisable en paramètre de fonction. En revanche, sa valeur peut être déplacée, c’est-à-dire qu’elle peut passer à un autre pointeur. Le pointeur de départ prend alors la valeur null et ainsi l’adresse pointée relève toujours d’un pointeur unique. L’utilisation de la classe std::unique_ptr nécessite l’inclusion :

#include <memory> 

1. Expérimentation du pointeur unique

Le template de la classe std::unique_ptr prend deux formes, la première définit un pointeur pour un élément unique et la seconde une spécialisation pour un tableau :

template <class T, class D = default_delete<T>> class unique_ptr; 
template <class T, class D> class unique_ptr<T[],D>; 

Le type D désigne un possible destructeur à élaborer soi-même. Le destructeur par défaut std::default_delete<T> est équivalent à delete pour un élément unique et à delete[ ] pour un tableau.

Soit une classe point :

#include <iostream> 
#include <memory> 

using namespace std;  // se passer de std:: dans la suite du code 
struct point { 
    int x, y; 
    void affiche() { cout << '(' << x << ',' << y << ")\n"; } 
}; 

Déclarer des unique_ptr. Par défaut, le pointeur vaut nullptr mais on peut préférer le spécifier :

int main() 
{ 
    unique_ptr<point>...

La classe std::shared_ptr

Un shared_ptr autorise plusieurs pointeurs à partager une même adresse mémoire (shared signifie partagé). Mais à la différence des pointeurs ordinaires, le nombre des occurrences de pointeurs partageant la même adresse est compté et la libération de la mémoire pointée ne s’effectue qu’avec le dernier pointeur resté actif. L’utilisation de la classe std::shared_ptr nécessite l’inclusion :

#include <memory> 

1. Expérimentation du pointeur partagé

Dans un programme, il y a toujours des difficultés à gérer plusieurs pointeurs qui pointent simultanément sur un même espace mémoire. Notamment, lors de la désallocation de l’espace mémoire pointé, retrouver l’ensemble des pointeurs concernés et les mettre à jour peut constituer une vraie difficulté. En oublier un peut occasionner un access violation. C’est la raison d’être des shared_ptr : faciliter la gestion de telles situations. L’idée principale consiste à compter le nombre de pointeurs pointant un même élément. Chaque nouveau pointeur affecté à la cible augmente le compte de 1 et chaque pointeur supprimé le diminue de 1. La désallocation de l’espace mémoire pointé dépend de la programmation du destructeur, qui est à fournir sous forme d’objets fonctions ou d’expressions lambda. En principe, elle ne devrait s’effectuer qu’avec le dernier pointeur restant, sauf situations très particulières.

Le template de la classe shared_ptr prend la forme suivante :

template <class T> class shared_ptr; 

Pour déclarer et allouer un shared_ptr, nous avons besoin de la bibliothèque :

#include...

La classe std::weak_ptr

La classe weak_ptr (weak signifie faible, simple, léger) est un pointeur utilisé en conjonction avec des pointeurs shared_ptr qui n’est pas comptabilisé dans l’ensemble constitué des pointeurs partagés. Il sert dans certaines situations à observer un shared_ptr sans interférer avec le comptage des occurrences pointées. Il n’accède jamais directement à l’élément sur lequel il pointe et tient automatiquement compte de sa libération mémoire. L’utilisation de la classe std::weak_ptr nécessite l’inclusion :

#include <memory> 

1. Expérimentation du pointeur "simple observateur"

Parmi les pointeurs intelligents, le weak pointer se traduit aussi par "pointeur simple d’esprit" (sans doute faut-il y voir un peu d’humour). Il s’utilise en complément d’un ou plusieurs shared_ptr propriétaires d’un même objet mais lui-même reste exclu de la copropriété. Il ne dispose d’aucun moyen d’entrer dans la gestion de l’objet pointé, il n’offre aucun accès possible à ses valeurs et n’est pas comptabilisé dans le retour du compteur use_count() des pointeurs partagés. Une fois qu’il est initialisé avec un pointeur partagé, il permet uniquement les actions suivantes :

  • use_count() retourne le nombre de pointeurs partagés pointant sur le même objet, mais le weak_ptr en est exclu.

  • expired() vérifie si l’objet pointé a déjà été supprimé.

  • lock() retourne un nouveau pointeur partagé shared_ptr qui, lui, entre dans la gestion de l’adresse référencée.

  • owner_before() donne une indication sur l’ordre d’implémentation du pointeur...

Allocateur de mémoire : la classe std::allocator

La classe std::allocator de la bibliothèque <memory> fournit l’allocateur par défaut utilisé pour les conteneurs C++ standards lorsqu’aucun autre n’est spécifié par l’utilisateur (array, vector, list, etc.). Le fait de pouvoir fournir un procédé d’allocation mémoire sous forme d’objet permet de l’adapter avec une grande souplesse dans le cadre de développements sophistiqués.

Depuis C++20, la classe est générique avec le modèle suivant :

template< class T > 
struct allocator; 
  • Elle contient en propriété les éléments :

    value_type : défini par T, correspond au type de l’élément.

    size_type : défini par size_t, conserve la quantité d’éléments.

    difference_type : défini par ptrdiff_t, donne la différence entre deux pointeurs.

    propagate_on_container_move_assignment : défini par true_ type, concerne l’affectation de déplacement d’un conteneur vers un autre.

  • Outre des constructeurs et un destructeur ses fonctions membres sont :

    allocate : alloue un espace mémoire non initialisé.

    allocate_at_least (C++23) : alloue un espace mémoire non initialisé et garantit une taille passée en paramètre.

    deallocate : désalloue un espace mémoire passé en paramètre.

  • En dehors de la classe se trouve uniquement une surcharge de l’opérateur d’égalité (==).

Expérimentation:

#include <iostream> 
#include <memory> 
int main()  
{ 
    // équivalent allocateur par défaut 
    std::allocator<double> alloc; 
    std::cout...