🔝 Retour au Sommaire
Il existe plusieurs façons de créer un std::unique_ptr. Toutes nécessitent l'inclusion du header <memory>.
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_uniqueest préférable à la construction directe (exception safety, lisibilité, cohérence) sont détaillées en section 9.3.
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.
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 ».
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 destructionCependant, 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 acceptableUne 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 -> :
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 ununique_ptrnul produit un comportement indéfini, exactement comme déréférencer un pointeur brut nul. Vérifiez toujours la validité si leunique_ptrpeut être nul dans votre logique.
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 deleteget() 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 brutRè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 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.
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() 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() 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| Méthode | Libère la ressource ? | Le unique_ptr devient nul ? | Qui gère la mémoire ensuite ? |
|---|---|---|---|
reset() |
Oui — delete 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 |
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 |
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éfiniCe 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étaireLa 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 utiliserstd::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)ouvoid 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*ouT&plutôt que des smart pointers. Ne passez un smart pointer que si la fonction participe à la gestion de la propriété.