Parcours
/
C++
C++
/
Programme
/
Pointeurs intelligents
Po

Pointeurs intelligents en C++

1 exercice

À propos de Pointeurs intelligents

Les pointeurs intelligents sont une fonctionnalité moderne du C++ conçue pour assurer une gestion automatique de la mémoire, ce qui aide à éviter les fuites de mémoire et les pointeurs pendants que l'on associe couramment aux pointeurs bruts. Ils servent d'enveloppes autour des pointeurs bruts, en y ajoutant des fonctionnalités supplémentaires comme la libération automatique de la mémoire lorsque le pointeur n'est plus nécessaire.

Syntaxe générale

Les pointeurs intelligents sont généralement implémentés sous forme de gabarits de classe dans la bibliothèque standard du C++. Les deux pointeurs intelligents les plus utilisés sont std::unique_ptr et std::shared_ptr.

Pointeurs uniques

std::unique_ptr est un pointeur intelligent qui possède l'objet de manière exclusive. Il garantit qu'à tout instant, un seul objet std::unique_ptr possède la ressource. Lorsque le std::unique_ptr propriétaire est détruit ou réinitialisé, il détruit automatiquement l'objet et libère sa mémoire.

#include <memory>
// Declaring and defining a unique pointer
auto rightful_king_of_england = std::make_unique<std::string>("Excalibur");

// Unique pointers cannot be copied or assigned
auto mordred = rightful_king_of_england; // Error: Cannot copy a unique_ptr

Avantages de std::make_unique()

Lorsque tu crées un std::unique_ptr, il est préférable d'utiliser std::make_unique() plutôt que d'allouer la mémoire directement avec new. std::make_unique() présente plusieurs avantages :

  1. Sécurité vis-à-vis des exceptions : std::make_unique() garantit la sécurité vis-à-vis des exceptions. Si une exception est levée pendant la construction de l'objet, la mémoire est automatiquement libérée, ce qui évite les fuites de mémoire.
  2. Clarté : utiliser std::make_unique() rend le code plus clair et plus concis. Plus besoin de préciser explicitement le type alloué, puisque les arguments de gabarit sont déduits automatiquement.
  3. Possibilités d'optimisation : les compilateurs peuvent optimiser std::make_unique() plus efficacement qu'une allocation manuelle de mémoire avec new, ce qui peut améliorer les performances.
  4. Éviter les mauvais usages : il est possible de supprimer la ressource sous-jacente lorsque le std::unique_ptr est construit manuellement. Cela conduirait à un comportement indéfini lorsque le std::unique_ptr tente de la supprimer à la fin de sa portée.

Pointeurs partagés

std::shared_ptr est un pointeur intelligent qui permet à plusieurs objets std::shared_ptr de partager la propriété d'une même ressource. Il garde la trace du nombre de pointeurs partagés qui référencent la ressource et ne libère la mémoire que lorsque le dernier pointeur partagé propriétaire de la ressource sort de sa portée ou est réinitialisé.

// Declaring and defining a shared pointer to a dynamically allocated string
auto martian_congressional_republic = std::make_shared<std::string>("protomolecule");

// Creating more shared pointer that shares ownership
auto outer_planets_alliance = martian_congressional_republic;
auto united_nations = martian_congressional_republic;
Caution

En C++17 et antérieur, l'utilisation de std::shared_ptr avec des tableaux via std::make_shared<T[]> n'est pas prise en charge directement. Bien qu'il soit possible d'allouer des tableaux avec std::make_shared<T[]>, créer des pointeurs partagés directement à partir de ceux-ci peut conduire à un comportement indéfini, en raison des différences de gestion de la mémoire entre les objets seuls et les tableaux. À la place, envisage d'utiliser std::vector ou des fonctions de suppression personnalisées pour gérer efficacement des tableaux avec des pointeurs partagés. Assure-toi toujours de la compatibilité avec ton compilateur et l'implémentation de ta bibliothèque standard lorsque tu manipules des allocations de tableaux et des pointeurs partagés en C++17.

Avantages de std::make_shared()

Tout comme std::make_unique(), std::make_shared() offre des avantages tels qu'une meilleure efficacité mémoire, la sécurité vis-à-vis des exceptions et la lisibilité. Il regroupe en une seule opération l'allocation de mémoire du bloc de contrôle et celle de l'objet géré, ce qui améliore l'efficacité et réduit le risque de fuites de mémoire. De plus, la déduction automatique des arguments de gabarit simplifie le code et améliore sa lisibilité. Utiliser std::make_shared() favorise un code plus propre, plus sûr et plus efficace lorsque tu travailles avec des objets std::shared_ptr en C++.

Pointeurs faibles

std::weak_ptr est une classe compagne de std::shared_ptr qui fournit une référence « faible » non propriétaire à un objet géré par un pointeur partagé. Il permet d'accéder à la ressource sans affecter sa durée de vie. Les pointeurs faibles sont utiles dans les cas où il faut briser des références cycliques pour éviter les fuites de mémoire.

// Creating a shared pointer
auto your_account = std::make_shared<std::string>("secret_subscription_password");
// Creating a shared pointer that shares ownership
auto your_flatmates_account = your_account;

// Creating a weak pointer from the shared pointer
auto your_flatmates_boyfriends_account = your_flatmates_account;
// if your_account and your_flatmates_account are deleted, there is no more reference to the shared pointer.
// your_flatmates_boyfriends_account will be a null pointer and cannot use the associated object any longer.

std::weak_ptr et la propriété cyclique

std::weak_ptr a été conçu pour répondre au problème de la propriété cyclique, aussi appelée références circulaires, qui peut survenir lors de l'utilisation de std::shared_ptr.

Dans un scénario de propriété cyclique, deux objets std::shared_ptr ou plus se référencent mutuellement, créant un cycle dans lequel aucun des objets ne peut être supprimé, car ils détiennent des références fortes l'un vers l'autre, ce qui entraîne des fuites de mémoire.

std::weak_ptr apporte une solution à ce problème en permettant des références faibles vers des objets partagés sans contribuer à leur compteur de références. Cela signifie qu'il peut observer et accéder à l'objet partagé sans empêcher sa suppression. Si toutes les références fortes vers l'objet partagé sont libérées, l'objet est détruit et les pointeurs faibles qui l'observent sont automatiquement réinitialisés à nullptr.

Voici un court exemple qui montre comment utiliser std::weak_ptr pour briser une propriété cyclique :

struct Node {
    Node() { std::cout << "Node constructed\n"; }
    ~Node() { std::cout << "Node destructed\n"; }

    std::weak_ptr<Node> next;
};

// Creating two nodes
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();

// Creating a cycle by making each node point to the other
node1->next = node2;
node2->next = node1;

// Release strong references to nodes
node1.reset();
node2.reset();

// Nodes are automatically destructed due to weak references

« Node constructed » est affiché deux fois, suivi de deux lignes « Node destructed ».

Lorsque next n'est pas un pointeur faible mais un std::shared_ptr, cela crée une situation de propriété cyclique, ce qui entraîne des fuites de mémoire, car les deux nœuds détiennent des références fortes l'un vers l'autre et empêchent ainsi leur destruction.

struct Node {
    Node() { std::cout << "Node constructed\n"; }
    ~Node() { std::cout << "Node destructed\n"; }

    std::shared_ptr<Node> next; // Change to shared_ptr
};

Dans ce scénario, node1 et node2 détiennent tous les deux des références fortes l'un vers l'autre via std::shared_ptr, ce qui forme un cycle. Lorsque le programme se termine et tente de libérer les références fortes avec reset(), les compteurs de références des deux nœuds restent non nuls, car chaque nœud détient une référence forte vers l'autre. Par conséquent, les nœuds ne sont pas détruits, ce qui entraîne des fuites de mémoire. « Node constructed » est affiché deux fois, mais n'est suivi d'aucune ligne « Node destructed ».

Pointeurs pendants et std::weak_ptr

Un pointeur pendant apparaît lorsqu'un pointeur référence un objet qui a été supprimé, ce qui conduit à un comportement indéfini lorsque l'on déréférence ce pointeur. std::weak_ptr aide à éviter les pointeurs pendants en fournissant une référence faible, non propriétaire, vers un objet géré par std::shared_ptr. Voici comment éviter les pointeurs pendants avec std::weak_ptr :

  1. Crée un std::weak_ptr : au lieu de détenir directement un std::shared_ptr, crée un std::weak_ptr vers l'objet partagé.

  2. Vérifie sa validité : avant d'utiliser le std::weak_ptr, vérifie sa validité avec la fonction expired(). Cette fonction renvoie true si l'objet partagé associé a été supprimé.

  3. Verrouille le std::weak_ptr : pour accéder à l'objet partagé en toute sécurité, utilise la fonction lock(), qui renvoie un std::shared_ptr pointant vers le même objet s'il est encore valide. Si l'objet a été supprimé, lock() renvoie un std::shared_ptr vide.

Voici un exemple de code qui montre comment utiliser std::weak_ptr pour éviter les pointeurs pendants :

auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();

// Create weak pointers
node1->next = node2;
node2->next = node1;

// Check validity and lock
if (auto lockedNode2 = node1->next.lock()) {
    // Use lockedNode2 safely
} else {
    // Handle case where node2 has been deleted
}

Dans cet exemple, node1->next et node2->next sont des objets std::weak_ptr. Avant d'accéder à l'objet partagé, on vérifie sa validité avec la fonction expired(). Ensuite, on utilise lock() pour accéder à l'objet partagé en toute sécurité. Cette approche garantit que les pointeurs pendants sont évités lorsque l'on travaille avec std::weak_ptr.

Conseils d'utilisation

Utilise les pointeurs intelligents par défaut : std::unique_ptr pour une propriété exclusive et std::shared_ptr pour une propriété partagée. Réserve les pointeurs bruts aux références non propriétaires ou aux interactions avec du code ancien. Dans la plupart des cas, std::unique_ptr suffit pour une propriété exclusive, car il offre une gestion de la mémoire légère, sans le surcoût du comptage de références. std::shared_ptr doit être utilisé avec parcimonie, car il introduit un surcoût et de la complexité, sauf si une véritable propriété partagée est nécessaire. std::weak_ptr est spécialisé pour briser les dépendances cycliques ou observer des objets partagés, mais il est rarement utilisé.

Modifie via GitHub Le lien s'ouvre dans une nouvelle fenêtre ou un nouvel onglet

Apprends Pointeurs intelligents