Skip to content

Latest commit

 

History

History
352 lines (240 loc) · 13.7 KB

File metadata and controls

352 lines (240 loc) · 13.7 KB

🔝 Retour au Sommaire

9.1.1 Création et utilisation

Créer un std::unique_ptr

Il existe plusieurs façons de créer un std::unique_ptr. Toutes nécessitent l'inclusion du header <memory>.

La méthode recommandée : std::make_unique (C++14)

std::make_unique est la manière idiomatique et sûre de créer un unique_ptr. La fonction alloue l'objet sur le tas et retourne directement un unique_ptr qui le possède :

#include <memory>
#include <string>

// Entier initialisé à 42
auto p1 = std::make_unique<int>(42);

// String construite avec ses arguments
auto p2 = std::make_unique<std::string>("Bonjour");

// Objet construit par défaut (int initialisé à 0)
auto p3 = std::make_unique<int>();

// Objet avec constructeur multi-arguments
struct Point {
    double x, y, z;
    Point(double x, double y, double z) : x(x), y(y), z(z) {}
};

auto p4 = std::make_unique<Point>(1.0, 2.5, 3.7);

Les arguments passés à std::make_unique<T>(args...) sont transmis directement au constructeur de T par perfect forwarding. Vous n'avez jamais besoin d'écrire new vous-même.

💡 Les raisons profondes pour lesquelles std::make_unique est préférable à la construction directe (exception safety, lisibilité, cohérence) sont détaillées en section 9.3.

Construction directe (à éviter en général)

Il est techniquement possible de construire un unique_ptr en lui passant un pointeur brut :

// Fonctionne, mais déconseillé dans du code nouveau
std::unique_ptr<int> p(new int(42));

Cette forme existe pour deux raisons : elle est antérieure à std::make_unique (qui n'est arrivé qu'en C++14), et elle reste nécessaire dans certains cas spécifiques, notamment quand on utilise un custom deleter (section 9.1.3) ou quand on récupère un pointeur brut provenant d'une API C.

Création d'un unique_ptr nul

Un unique_ptr peut être explicitement nul — il ne possède alors aucune ressource :

std::unique_ptr<int> p1;            // Nul par défaut  
std::unique_ptr<int> p2(nullptr);   // Explicitement nul  
std::unique_ptr<int> p3 = nullptr;  // Équivalent  

Un unique_ptr nul ne consomme aucune mémoire sur le tas. Son destructeur ne fait rien. C'est un état parfaitement valide qui correspond au concept « je ne possède rien pour l'instant ».

Tableaux dynamiques : make_unique avec T[]

std::unique_ptr supporte les tableaux dynamiques via une spécialisation partielle unique_ptr<T[]>. L'appel à delete[] (et non delete) est automatiquement garanti :

// Tableau de 100 entiers, tous initialisés à zéro
auto tab = std::make_unique<int[]>(100);

tab[0] = 10;  
tab[99] = 42;  

// ✅ delete[] est appelé automatiquement à la destruction

Cependant, dans la pratique, préférez std::vector à un tableau dynamique brut. Un vector offre une taille connue à l'exécution, des vérifications de bornes (en mode debug), et une interface beaucoup plus riche :

// Préférez ceci dans la majorité des cas :
std::vector<int> tab(100, 0);

// Réservez unique_ptr<T[]> aux cas spécifiques :
// - Interfaçage avec du code C qui attend un buffer brut
// - Contraintes de performance où la surcharge de vector
//   (3 pointeurs internes) n'est pas acceptable

Accéder à la ressource

Une fois le unique_ptr créé, vous accédez à la ressource qu'il possède exactement comme avec un pointeur brut, via les opérateurs * et -> :

Déréférencement : operator* et operator->

auto p = std::make_unique<std::string>("Hello, C++");

// operator* : accès à l'objet pointé
std::string& ref = *p;  
std::print("{}\n", *p);  // "Hello, C++"  

// operator-> : accès aux membres
std::print("Taille : {}\n", p->size());    // 10  
std::print("Vide ? {}\n", p->empty());     // false  

Ces opérateurs sont inlinés par le compilateur : le code machine produit est strictement identique à celui d'un pointeur brut. Il n'y a aucune indirection supplémentaire.

⚠️ Comportement indéfini : déréférencer un unique_ptr nul produit un comportement indéfini, exactement comme déréférencer un pointeur brut nul. Vérifiez toujours la validité si le unique_ptr peut être nul dans votre logique.

Accès au pointeur brut sous-jacent : get()

La méthode get() retourne le pointeur brut encapsulé sans transférer la propriété :

auto p = std::make_unique<int>(42);

int* raw = p.get();  
std::print("Valeur : {}\n", *raw);   // 42  
std::print("Adresse : {}\n", static_cast<void*>(raw));  

// p possède toujours la ressource
// raw est un observateur — il ne doit PAS appeler delete

get() est indispensable pour interagir avec des API qui attendent un pointeur brut — ce qui est très courant avec les bibliothèques C et certaines API système :

// Exemple : API C qui attend un pointeur brut
extern "C" void traiter_buffer(const char* data, size_t len);

auto buffer = std::make_unique<char[]>(1024);
// ... remplir le buffer ...
traiter_buffer(buffer.get(), 1024);  // ✅ Passage du pointeur brut

Règle fondamentale : le pointeur retourné par get() ne doit jamais être utilisé pour libérer la mémoire (delete) ni pour construire un autre smart pointer. La propriété reste au unique_ptr d'origine.

auto p = std::make_unique<int>(42);  
int* raw = p.get();  

delete raw;                             // ❌ Double free à la destruction de p  
std::unique_ptr<int> autre(raw);        // ❌ Double free — deux propriétaires  
std::shared_ptr<int> partage(raw);      // ❌ Même problème  

Vérifier la nullité

Un unique_ptr peut être nul (après un déplacement, après un reset(), ou s'il a été construit sans argument). Plusieurs syntaxes permettent de tester cet état :

auto p = std::make_unique<int>(42);

// Conversion implicite en bool — la forme idiomatique
if (p) {
    std::print("p possède une ressource : {}\n", *p);
}

// Comparaison explicite avec nullptr
if (p != nullptr) {
    std::print("p n'est pas nul\n");
}

// Après un déplacement
auto q = std::move(p);

if (!p) {
    std::print("p est maintenant vide\n");   // ✅ Affiché
}

if (q) {
    std::print("q possède la ressource : {}\n", *q);  // 42
}

La forme if (p) est la plus concise et la plus idiomatique. Elle repose sur l'opérateur explicit operator bool() défini par unique_ptr.


Modifier l'état : reset() et release()

Au-delà de la création et de l'accès, unique_ptr expose deux méthodes qui modifient la relation de propriété. Elles servent des objectifs très différents et il est essentiel de ne pas les confondre.

reset() : libérer et éventuellement remplacer

reset() détruit la ressource actuellement possédée et la remplace éventuellement par une nouvelle :

auto p = std::make_unique<std::string>("Premier");

// Libère "Premier", p possède maintenant "Deuxième"
p.reset(new std::string("Deuxième"));

// Libère "Deuxième", p devient nul
p.reset();

// Équivalent :
p = nullptr;

reset() est l'équivalent sûr de « je n'ai plus besoin de cette ressource ». Le delete est appelé automatiquement sur l'ancienne ressource avant que le unique_ptr ne prenne possession de la nouvelle (ou devienne nul).

Un cas d'usage fréquent est la libération anticipée d'une ressource coûteuse :

void traitement_long() {
    auto donnees = std::make_unique<GrosBuffer>();
    
    // Phase 1 : utiliser les données
    analyser(*donnees);

    // Libérer la mémoire dès qu'elle n'est plus nécessaire,
    // plutôt qu'attendre la fin du scope
    donnees.reset();

    // Phase 2 : autre traitement qui n'a pas besoin du buffer
    generer_rapport();
}

release() : abandonner la propriété sans libérer

release() est fondamentalement différent de reset(). Il détache le pointeur brut du unique_ptr sans appeler delete. L'appelant récupère le pointeur brut et devient responsable de la libération :

auto p = std::make_unique<int>(42);

int* raw = p.release();
// p est maintenant nul
// raw pointe vers l'entier — VOUS devez le libérer

// ... utilisation de raw ...

delete raw;  // Obligation de l'appelant

⚠️ release() est une opération dangereuse si elle est mal utilisée. Le pointeur retourné doit être libéré manuellement ou confié à un autre gestionnaire de ressource. Si vous oubliez, c'est un memory leak.

release() existe principalement pour deux raisons :

1. Transférer la propriété vers une API C qui prend en charge la libération :

// API C qui prend la propriété et appelle free() plus tard
extern "C" void register_callback(void* data, void (*cleanup)(void*));

auto ctx = std::make_unique<CallbackContext>();
// L'API prend la propriété — on ne veut pas que unique_ptr fasse delete
register_callback(ctx.release(), &liberer_contexte);

2. Transférer vers un autre smart pointer de type différent :

std::unique_ptr<Derived> derived = std::make_unique<Derived>();
// Transfert vers un unique_ptr de type base
std::unique_ptr<Base> base(derived.release());

Cependant, pour ce second cas, préférez la conversion implicite qui est plus sûre et plus lisible :

std::unique_ptr<Base> base = std::make_unique<Derived>();  // ✅ Conversion implicite

Comparaison reset() vs release()

Méthode Libère la ressource ? Le unique_ptr devient nul ? Qui gère la mémoire ensuite ?
reset() Ouidelete est appelé Oui Personne (déjà libérée), ou le unique_ptr si un nouveau pointeur est passé
reset(new T(...)) Oui sur l'ancienne Non — possède la nouvelle Le unique_ptr
release() Non — pas de delete Oui L'appelant — responsabilité manuelle

Résumé de l'interface essentielle

Voici une vue synthétique des opérations courantes sur std::unique_ptr<T> :

Opération Syntaxe Effet
Création recommandée auto p = std::make_unique<T>(args...) Alloue et construit T sur le tas
Création directe std::unique_ptr<T> p(new T(args...)) Encapsule un pointeur brut existant
Création nulle std::unique_ptr<T> p; unique_ptr vide, ne possède rien
Déréférencement *p Accès à l'objet pointé
Accès membre p->membre Accès aux membres de l'objet
Pointeur brut p.get() Retourne T* sans transfert de propriété
Test de nullité if (p) true si p possède une ressource
Libérer et remplacer p.reset(new_ptr) Libère l'ancien, prend possession du nouveau
Libérer et vider p.reset() Libère la ressource, p devient nul
Abandonner la propriété p.release() Retourne T*, p devient nul, pas de delete
Échanger p.swap(q) Échange les ressources entre p et q

Piège classique : ne jamais créer deux unique_ptr sur le même pointeur brut

C'est l'erreur la plus grave et la plus insidieuse avec les smart pointers. Deux unique_ptr ne doivent jamais posséder le même pointeur brut :

int* raw = new int(42);

std::unique_ptr<int> p1(raw);  
std::unique_ptr<int> p2(raw);  // ❌ CATASTROPHE  

// À la destruction :
// p2 appelle delete raw → OK
// p1 appelle delete raw → DOUBLE FREE → comportement indéfini

Ce bug ne produit pas d'erreur de compilation. Il compile silencieusement et corrompt la mémoire à l'exécution. C'est précisément pour éviter cette situation que std::make_unique est recommandé : en n'exposant jamais le pointeur brut intermédiaire, vous éliminez structurellement le risque.

// ✅ Avec make_unique, le pointeur brut n'est jamais visible
auto p = std::make_unique<int>(42);
// Impossible de créer accidentellement un second propriétaire

Interaction avec les fonctions

La manière dont vous passez un unique_ptr à une fonction — ou dont vous le retournez — exprime une intention précise sur la propriété. Ce sujet est traité en détail dans la section suivante (9.1.2 — Transfert de propriété avec std::move), mais voici les principes directeurs :

  • Passer par valeur (void f(std::unique_ptr<T> p)) signifie « la fonction prend la propriété ». L'appelant doit utiliser std::move.
  • Passer par référence (void f(std::unique_ptr<T>& p)) signifie « la fonction peut modifier la propriété » (reset, swap). C'est assez rare.
  • Passer le pointeur brut (void f(T* p) ou void f(T& ref)) signifie « la fonction utilise la ressource sans s'occuper de la propriété ». C'est le cas le plus courant pour les fonctions qui se contentent de lire ou modifier l'objet.
  • Retourner un unique_ptr (std::unique_ptr<T> f()) signifie « la fonction crée une ressource et en transfère la propriété à l'appelant ». Le déplacement est implicite grâce au RVO (section 10.5).
// La fonction crée et transfère la propriété
std::unique_ptr<Widget> creer_widget(int id) {
    return std::make_unique<Widget>(id);  // RVO, pas de std::move nécessaire
}

// La fonction utilise l'objet sans le posséder
void afficher_widget(const Widget& w) {
    std::print("Widget: {}\n", w.nom());
}

// Utilisation
auto widget = creer_widget(1);  
afficher_widget(*widget);  // Passage par déréférencement  

Règle C++ Core Guidelines (F.7) : pour les paramètres d'usage général, préférez T* ou T& plutôt que des smart pointers. Ne passez un smart pointer que si la fonction participe à la gestion de la propriété.

⏭️ Transfert de propriété avec std::move