🔝 Retour au Sommaire
Un std::unique_ptr ne peut pas être copié — c'est la garantie qui assure l'unicité de la possession. Mais dans un programme réel, les ressources doivent circuler : une fonction crée un objet et le confie à une autre, un conteneur stocke des ressources créées ailleurs, un objet est transféré d'un propriétaire à un autre.
C'est le rôle de std::move : il convertit un unique_ptr en rvalue reference, signalant au compilateur que l'objet source peut être « vidé » au profit de la destination. Après le déplacement, le unique_ptr source est dans un état valide mais vide — il vaut nullptr.
#include <memory>
auto source = std::make_unique<int>(42);
// Transfert de propriété : source → destination
auto destination = std::move(source);
// source est maintenant nul
// destination possède l'entierCe transfert a trois propriétés essentielles :
- Il est explicite :
std::moverend le transfert visible dans le code. Aucune surprise silencieuse (contrairement à l'ancienstd::auto_ptr). - Il est gratuit : le déplacement d'un
unique_ptrcopie un pointeur (8 octets) et met l'ancien ànullptr. Pas d'allocation, pas de copie de données. - Il est irréversible : une fois déplacé, le
unique_ptrsource ne possède plus rien. Tenter de le déréférencer est un comportement indéfini.
Quand une fonction reçoit un unique_ptr par valeur, elle prend la propriété de la ressource. L'appelant doit explicitement utiliser std::move pour signaler qu'il abandonne la possession :
class Logger {
std::string nom_;
public:
explicit Logger(std::string nom) : nom_(std::move(nom)) {}
void log(const std::string& msg) {
std::print("[{}] {}\n", nom_, msg);
}
};
// La fonction prend la propriété du Logger
void enregistrer_logger(std::unique_ptr<Logger> logger) {
// logger est possédé par cette fonction
logger->log("Enregistré avec succès");
// logger est détruit à la fin du scope
}
int main() {
auto logger = std::make_unique<Logger>("App");
logger->log("Avant transfert"); // ✅ OK — logger est encore valide
enregistrer_logger(std::move(logger)); // Transfert de propriété
// ⚠️ logger est maintenant nul
// logger->log("Après"); // ❌ Comportement indéfini !
if (!logger) {
std::print("logger a été transféré\n"); // ✅ Affiché
}
}La signature void f(std::unique_ptr<T> p) est un contrat dans le code : elle dit « cette fonction consomme la ressource — vous ne l'aurez plus après l'appel ». Si vous oubliez std::move, le compilateur refuse :
auto p = std::make_unique<int>(42);
enregistrer_logger(p); // ❌ Erreur : copie impossible
enregistrer_logger(std::move(p)); // ✅ Transfert explicite Si une fonction se contente de lire ou modifier l'objet sans en prendre la propriété, ne passez pas de smart pointer. Passez une référence ou un pointeur brut :
// ✅ BON — la fonction observe sans posséder
void afficher(const Widget& w) {
std::print("Widget: {}\n", w.id());
}
// ❌ MAUVAIS — prend inutilement la propriété
void afficher(std::unique_ptr<Widget> w) {
std::print("Widget: {}\n", w->id());
// L'appelant a perdu son widget pour rien
}
// ✅ BON — pointeur brut si la nullité est possible
void traiter_si_present(Widget* w) {
if (w) {
w->process();
}
}
// Utilisation
auto widget = std::make_unique<Widget>(1);
afficher(*widget); // Déréférencement → référence
traiter_si_present(widget.get()); // get() → pointeur brut non-possédant
// widget est toujours valide et possédé par main()Cette distinction est résumée dans les C++ Core Guidelines :
| Ce que la fonction fait | Type du paramètre | Appel |
|---|---|---|
| Utilise l'objet (non nullable) | const T& ou T& |
f(*ptr) |
| Utilise l'objet (nullable) | T* |
f(ptr.get()) |
| Prend la propriété | std::unique_ptr<T> |
f(std::move(ptr)) |
| Peut reseater le pointeur | std::unique_ptr<T>& |
f(ptr) |
C'est le pattern le plus naturel et le plus élégant : une fonction crée une ressource et en transfère la propriété à l'appelant. Le compilateur gère le transfert automatiquement — pas besoin d'écrire std::move dans le return :
std::unique_ptr<Widget> creer_widget(int id, const std::string& nom) {
auto w = std::make_unique<Widget>(id, nom);
w->initialiser();
return w; // ✅ Déplacement implicite (NRVO/move)
}
int main() {
auto widget = creer_widget(1, "Dashboard");
widget->afficher(); // L'appelant possède le widget
}Le compilateur applique ici la Named Return Value Optimization (NRVO) ou, si celle-ci n'est pas possible, un déplacement implicite. Dans les deux cas, aucune copie n'a lieu. Écrire return std::move(w) est non seulement inutile mais contre-productif : cela empêche le NRVO, qui est encore plus efficace qu'un déplacement (voir section 10.5).
return std::move(w); // ⚠️ Inutile et contre-productif — empêche le NRVO
return w; // ✅ Laisse le compilateur optimiser Ce mécanisme est la base du factory pattern en C++ moderne. La factory crée l'objet et transfère la propriété, rendant le contrat limpide :
class Animal {
public:
virtual ~Animal() = default;
virtual std::string cri() const = 0;
};
class Chat : public Animal {
public:
std::string cri() const override { return "Miaou"; }
};
class Chien : public Animal {
public:
std::string cri() const override { return "Wouf"; }
};
// Factory : retourne un unique_ptr vers la classe de base
std::unique_ptr<Animal> creer_animal(std::string_view type) {
if (type == "chat") return std::make_unique<Chat>();
if (type == "chien") return std::make_unique<Chien>();
return nullptr; // ✅ Type inconnu — unique_ptr nul
}
int main() {
auto animal = creer_animal("chat");
if (animal) {
std::print("{}\n", animal->cri()); // "Miaou"
}
}Notez la conversion implicite de std::unique_ptr<Chat> vers std::unique_ptr<Animal>. Cette conversion fonctionne tant que Chat hérite publiquement de Animal et que Animal a un destructeur virtuel — exactement comme pour les pointeurs bruts.
Les conteneurs de la STL supportent les types move-only depuis C++11. Vous pouvez stocker des unique_ptr dans un std::vector, un std::map, une std::list, etc.
std::vector<std::unique_ptr<Animal>> zoo;
// emplace_back construit directement dans le vector
zoo.push_back(std::make_unique<Chat>());
zoo.push_back(std::make_unique<Chien>());
// Ajout d'un unique_ptr existant — std::move obligatoire
auto perroquet = std::make_unique<Perroquet>();
zoo.push_back(std::move(perroquet));
// perroquet est maintenant nul
// Parcours — par référence constante, pas de transfert
for (const auto& animal : zoo) {
std::print("{}\n", animal->cri());
}push_back et emplace_back déclenchent le constructeur de déplacement du unique_ptr. Le vector devient le propriétaire de chaque ressource. À la destruction du vector, tous les objets pointés sont automatiquement libérés.
Extraire un unique_ptr d'un conteneur nécessite de le déplacer — le conteneur perd alors la possession de cet élément :
// Extraire le dernier élément
auto dernier = std::move(zoo.back());
zoo.pop_back(); // Supprime le unique_ptr vide du vector
// Extraire un élément à un index donné
auto deuxieme = std::move(zoo[1]);
// ⚠️ zoo[1] est maintenant nul — le vector contient un "trou"
// Il faut nettoyer : supprimer l'entrée nulle
zoo.erase(zoo.begin() + 1);💡 Si vous avez fréquemment besoin d'extraire des éléments du milieu d'un vector de
unique_ptr, c'est peut-être le signe qu'un autre conteneur serait plus adapté (std::list,std::deque), ou que la logique d'accès devrait être repensée.
std::map<std::string, std::unique_ptr<Config>> configurations;
// Insertion
configurations["dev"] = std::make_unique<Config>("dev.yaml");
configurations["prod"] = std::make_unique<Config>("prod.yaml");
// Accès — par référence, jamais par copie
const auto& config_dev = configurations["dev"];
if (config_dev) {
std::print("Port: {}\n", config_dev->port());
}
// Extraction avec extract() (C++17) — propre et efficace
auto node = configurations.extract("dev");
if (!node.empty()) {
auto config = std::move(node.mapped());
// config possède maintenant la Config "dev"
}Le transfert de propriété s'intègre naturellement avec le polymorphisme. Les conversions unique_ptr<Derived> → unique_ptr<Base> fonctionnent implicitement, comme pour les pointeurs bruts :
class Shape {
public:
virtual ~Shape() = default;
virtual double aire() const = 0;
};
class Cercle : public Shape {
double rayon_;
public:
explicit Cercle(double r) : rayon_(r) {}
double aire() const override { return 3.14159 * rayon_ * rayon_; }
};
class Rectangle : public Shape {
double largeur_, hauteur_;
public:
Rectangle(double l, double h) : largeur_(l), hauteur_(h) {}
double aire() const override { return largeur_ * hauteur_; }
};
// Conversion implicite Derived → Base
std::unique_ptr<Shape> s = std::make_unique<Cercle>(5.0); // ✅
// Dans un conteneur polymorphique
std::vector<std::unique_ptr<Shape>> formes;
formes.push_back(std::make_unique<Cercle>(3.0));
formes.push_back(std::make_unique<Rectangle>(4.0, 5.0));
double total = 0.0;
for (const auto& forme : formes) {
total += forme->aire();
}
std::print("Aire totale : {:.2f}\n", total);
⚠️ Rappel essentiel : la classe de base doit avoir un destructeur virtuel (virtual ~Shape() = default;). Sans cela, la destruction via ununique_ptr<Base>n'appellerait pas le destructeur de la classe dérivée — un comportement indéfini et une source de fuites de ressources. Voir section 7.2.
Après un std::move, le unique_ptr source est dans un état valide mais non spécifié. En pratique, pour unique_ptr, cet état est bien défini : il vaut nullptr. C'est garanti par le standard.
Cependant, la bonne pratique est de ne plus utiliser un unique_ptr après l'avoir déplacé, sauf pour le tester ou le réaffecter :
auto p = std::make_unique<int>(42);
auto q = std::move(p);
// Opérations sûres sur p après déplacement :
if (p == nullptr) { /* ... */ } // ✅ Test de nullité
p = std::make_unique<int>(100); // ✅ Réaffectation
p.reset(); // ✅ Reset (no-op si déjà nul)
// Opérations dangereuses :
// *p; // ❌ Comportement indéfini (déréférencement de nullptr)
// p->foo(); // ❌ Comportement indéfiniUn pattern courant et défensif consiste à limiter la portée du unique_ptr source pour que le compilateur empêche toute utilisation accidentelle :
void configurer_systeme(std::unique_ptr<Config> config);
void demarrer() {
auto config = charger_config("app.yaml");
// Validation avant transfert
if (!config->est_valide()) {
std::print("Configuration invalide\n");
return;
}
configurer_systeme(std::move(config));
// Après cette ligne, config est nul.
// Tout le code qui suit ne devrait pas toucher config.
}Il est tentant d'écrire du code qui déplace un unique_ptr dans certaines branches seulement. C'est techniquement valide mais dangereux car l'état du unique_ptr après le bloc conditionnel dépend du chemin d'exécution :
auto ressource = std::make_unique<Ressource>();
if (condition) {
consommer(std::move(ressource)); // Déplacé
} else {
modifier(*ressource); // Pas déplacé
}
// ⚠️ Ici, ressource est-il nul ou pas ? Ça dépend de condition.
// Ce code est fragile et difficile à maintenir.
ressource->faire_quelque_chose(); // 💥 Crash potentielLa solution est de structurer le code pour que le unique_ptr soit toujours dans un état connu après le bloc :
auto ressource = std::make_unique<Ressource>();
if (condition) {
consommer(std::move(ressource));
return; // ✅ Sortie anticipée — pas d'ambiguïté
}
// Ici, ressource est garanti non-nul
modifier(*ressource);
ressource->faire_quelque_chose(); // ✅ Sûr | Scénario | Syntaxe | std::move nécessaire ? |
|---|---|---|
| Passer en paramètre (transfert) | f(std::move(ptr)) |
Oui — explicite |
| Retourner depuis une fonction | return ptr; |
Non — implicite (NRVO/move) |
| Stocker dans un conteneur | vec.push_back(std::move(ptr)) |
Oui — explicite |
| Construire depuis une rvalue | auto p = creer(); |
Non — résultat temporaire |
| Conversion Derived → Base | unique_ptr<Base> b = std::move(d); |
Oui — explicite |
| Affecter à un autre unique_ptr | autre = std::move(ptr); |
Oui — explicite |
La règle est cohérente : chaque fois qu'un unique_ptr nommé (une lvalue) change de propriétaire, std::move est requis. Les valeurs temporaires (résultats de fonctions, make_unique directement dans un argument) sont déplacées implicitement.