Aller au contenu

std::unique_ptr : la propriété exclusive

Découvrez comment std::unique_ptr offre une propriété exclusive sans surcoût, comment utiliser make_unique, les suppresseurs personnalisés pour les ressources C et les fonctions fabriques.

Qu'est-ce que unique_ptr ?

std::unique_ptr est un pointeur intelligent qui possède un objet de manière exclusive — il existe exactement un unique_ptr pointant vers un objet donné. Lorsque le unique_ptr est détruit (sort de portée), il supprime automatiquement l'objet possédé.

Propriétés clés :
- Non copiable — une copie créerait deux propriétaires, ce qui violerait la propriété exclusive.
- Déplaçable — la propriété peut être transférée via std::move.
- Aucun surcoût — sizeof(unique_ptr) == sizeof(T*) (avec le suppresseur par défaut). Le code généré est identique à celui d'un pointeur brut avec un delete manuel.
- C'est le pointeur intelligent par défaut. Pensez d'abord à unique_ptr ; n'utilisez shared_ptr que si vous avez réellement besoin d'une propriété partagée.

make_unique : la seule bonne méthode

std::make_unique(args...) (C++14) est la seule bonne façon de créer un unique_ptr. C'est sûr vis-à-vis des exceptions, concis, et cela évite d'écrire new.

unique_ptr_basics.cpp
#include <iostream>
#include <memory>
#include <string>

struct Connection {
    std::string host;
    int port;
    Connection(std::string h, int p) : host(std::move(h)), port(p) {
        std::cout << "Connected to " << host << ":" << port << '\n';
    }
    ~Connection() {
        std::cout << "Disconnected from " << host << '\n';
    }
    void query(const std::string& sql) {
        std::cout << "Executing: " << sql << '\n';
    }
};

int main() {
    // CORRECT: use make_unique
    auto conn = std::make_unique<Connection>("localhost", 5432);
    conn->query("SELECT * FROM users");

    // Access the raw pointer (non-owning)
    Connection* raw = conn.get();  // does NOT transfer ownership

    // Transfer ownership
    auto conn2 = std::move(conn);
    // conn is now nullptr
    if (!conn) std::cout << "conn is null after move\n";

    // conn2 destroyed here — destructor called automatically
}  // Output: "Disconnected from localhost"

Suppresseurs personnalisés pour les ressources C

De nombreuses bibliothèques C retournent des pointeurs bruts qui doivent être libérés avec une fonction spécifique (et non delete). unique_ptr prend en charge les suppresseurs personnalisés (custom deleters) pour gérer ces cas. Le suppresseur fait partie du type, il n'entraîne donc aucun surcoût lorsqu'on utilise un pointeur de fonction ou une lambda sans état.

custom_deleter.cpp
#include <cstdio>
#include <memory>
#include <iostream>

// Custom deleter for FILE*
struct FileDeleter {
    void operator()(FILE* f) const {
        if (f) {
            std::fclose(f);
            std::cout << "File closed\n";
        }
    }
};

// Type alias for clarity
using UniqueFile = std::unique_ptr<FILE, FileDeleter>;

// Factory function
UniqueFile open_file(const char* path, const char* mode) {
    FILE* f = std::fopen(path, mode);
    if (!f) throw std::runtime_error("Cannot open file");
    return UniqueFile(f);
}

int main() {
    try {
        auto file = open_file("/tmp/test.txt", "w");
        std::fputs("Hello, RAII!\n", file.get());
        // file automatically closed when scope exits
    } catch (const std::exception& e) {
        std::cerr << e.what() << '\n';
    }

    // Lambda deleter (less common, but useful for one-offs)
    auto deleter = [](int* p) { std::cout << "custom free\n"; delete p; };
    std::unique_ptr<int, decltype(deleter)> p(new int(42), deleter);
}

unique_ptr avec des tableaux

std::unique_ptr gère un tableau alloué dynamiquement et appelle delete[] automatiquement. Il fournit operator[] au lieu de operator* et operator->.

auto arr = std::make_unique<int[]>(100);  // 100 ints initialisés à zéro
arr[0] = 42;

Cependant, std::vector est presque toujours préférable — il connaît sa taille, permet l'itération et est tout aussi efficace. N'utilisez unique_ptr que pour interagir avec des API C ou lorsque vous avez besoin d'un tampon non redimensionnable et non copiable.

Fonctions fabriques retournant un unique_ptr

Retourner un unique_ptr depuis une fonction fabrique est le schéma de propriété idiomatique en C++. L'appelant reçoit la propriété exclusive. S'il souhaite une propriété partagée, il peut effectuer la conversion : std::shared_ptr sp = factory();.

factory_unique_ptr.cpp
#include <memory>
#include <iostream>
#include <string>

class Shape {
public:
    virtual ~Shape() = default;
    virtual double area() const = 0;
    virtual std::string name() const = 0;
};

class Circle : public Shape {
    double radius_;
public:
    explicit Circle(double r) : radius_(r) {}
    double area() const override { return 3.14159 * radius_ * radius_; }
    std::string name() const override { return "Circle"; }
};

class Rectangle : public Shape {
    double w_, h_;
public:
    Rectangle(double w, double h) : w_(w), h_(h) {}
    double area() const override { return w_ * h_; }
    std::string name() const override { return "Rectangle"; }
};

// Factory — caller owns the result
std::unique_ptr<Shape> make_shape(const std::string& type, double a, double b = 0) {
    if (type == "circle")    return std::make_unique<Circle>(a);
    if (type == "rectangle") return std::make_unique<Rectangle>(a, b);
    return nullptr;  // unknown shape
}

int main() {
    auto s = make_shape("circle", 5.0);
    if (s) {
        std::cout << s->name() << " area: " << s->area() << '\n';
    }
    // s automatically deleted here
}
Piège

Ne passez jamais un unique_ptr par référence const à une fonction qui se contente d'observer l'objet — passez plutôt un pointeur brut ou une référence. Les paramètres unique_ptr signalent un transfert de propriété.

- void use(Widget* w) — observe, sans propriété
- void sink(std::unique_ptr w) — prend la propriété (à appeler avec std::move)
- void reseat(std::unique_ptr& w) — peut remplacer l'objet possédé

Ne construisez jamais deux unique_ptr à partir du même pointeur brut — les deux appelleront delete, ce qui provoque une double libération. C'est pour cela qu'on préfère make_unique : il n'y a aucun pointeur brut susceptible d'être réutilisé par erreur.

Bonne pratique

1. Utilisez unique_ptr par défaut — ce doit être votre premier choix pour l'allocation sur le tas.
2. Utilisez toujours make_unique — n'écrivez jamais unique_ptr(new T(...)).
3. Retournez un unique_ptr depuis les fabriques — il se convertit implicitement en shared_ptr si l'appelant en a besoin.
4. Passez par valeur pour transférer la propriété — void sink(unique_ptr p). L'appelant doit utiliser std::move.
5. Passez un pointeur brut ou une référence pour observer — void use(const T& obj) ou void use(T* obj).
6. Marquez le constructeur et l'opérateur d'assignation par déplacement noexcept — c'est crucial pour les types stockés dans des conteneurs.

Points clés
  • unique_ptr offre une propriété exclusive sans aucun surcoût par rapport aux pointeurs bruts
  • Créez-le toujours via std::make_unique(args...) — n'utilisez jamais de new brut
  • Non copiable mais déplaçable — la propriété se transfère via std::move
  • Les suppresseurs personnalisés permettent d'appliquer le RAII aux ressources C (FILE*, sockets, etc.)
  • Retournez un unique_ptr depuis les fonctions fabriques — il se convertit en shared_ptr si nécessaire
  • Passez un pointeur brut ou une référence lorsqu'une fonction se contente d'observer sans posséder

Quiz — Testez vos connaissances

(15 XP)

1. Quel est le surcoût de `std::unique_ptr<T>` par rapport à un `T*` brut (avec le suppresseur par défaut) ?

2. Comment devez-vous passer un `unique_ptr` à une fonction qui doit en prendre la propriété ?

3. Pourquoi préfère-t-on `std::make_unique` à `unique_ptr<T>(new T(...))` ?