Vai al contenuto

std::unique_ptr: proprietà esclusiva

Impara come std::unique_ptr fornisce una proprietà esclusiva a costo zero, come usare make_unique, i deleter personalizzati per le risorse C e i pattern delle funzioni factory.

Che cos'è unique_ptr?

std::unique_ptr è uno smart pointer che possiede un oggetto in modo esclusivo — esiste esattamente un unique_ptr che punta a un dato oggetto. Quando l'unique_ptr viene distrutto (esce dallo scope), distrugge automaticamente l'oggetto posseduto.

Proprietà principali:
- Non copiabile — copiarlo creerebbe due proprietari, violando la proprietà esclusiva.
- Spostabile — la proprietà può essere trasferita tramite std::move.
- Costo zero — sizeof(unique_ptr) == sizeof(T*) (con il deleter predefinito). Il codice generato è identico a quello di un puntatore grezzo con delete manuale.
- Questo è lo smart pointer predefinito. Scegli prima unique_ptr; usa shared_ptr solo quando ti serve davvero la proprietà condivisa.

make_unique: l'unico modo corretto

std::make_unique(args...) (C++14) è l'unico modo corretto di creare un unique_ptr. È a prova di eccezioni, conciso ed evita di scrivere 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"

Deleter personalizzati per le risorse C

Molte librerie C restituiscono puntatori grezzi che devono essere liberati con una funzione specifica (non con delete). unique_ptr supporta i deleter personalizzati per gestire questi casi. Il deleter diventa parte del tipo, quindi ha costo zero quando si usa un puntatore a funzione o una lambda senza stato.

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 con gli array

std::unique_ptr gestisce un array allocato dinamicamente e chiama automaticamente delete[]. Fornisce operator[] al posto di operator* e operator->.

auto arr = std::make_unique<int[]>(100);  // 100 int inizializzati a zero
arr[0] = 42;

Tuttavia, std::vector è quasi sempre preferibile — conosce la propria dimensione, supporta l'iterazione ed è altrettanto efficiente. Usa unique_ptr solo per interfacciarti con API C o quando ti serve un buffer non ridimensionabile e non copiabile.

Funzioni factory che restituiscono unique_ptr

Restituire un unique_ptr dalle funzioni factory è il pattern idiomatico del C++ per la proprietà. Il chiamante riceve la proprietà esclusiva. Se il chiamante vuole la proprietà condivisa, può convertirlo: 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
}
Insidia

Non passare mai un unique_ptr per riferimento const a una funzione che si limita a osservare l'oggetto — passa invece un puntatore grezzo o un riferimento. I parametri di tipo unique_ptr segnalano un trasferimento di proprietà.

- void use(Widget* w) — osserva, nessuna proprietà
- void sink(std::unique_ptr w) — acquisisce la proprietà (chiamala con std::move)
- void reseat(std::unique_ptr& w) — può sostituire l'oggetto posseduto

Non costruire mai due unique_ptr dallo stesso puntatore grezzo — entrambi chiameranno delete, causando un doppio rilascio. Ecco perché make_unique è preferibile: non esiste alcun puntatore grezzo da riutilizzare per sbaglio.

Buona pratica

1. Usa unique_ptr come scelta predefinita — dovrebbe essere la tua prima opzione per l'allocazione sull'heap.
2. Usa sempre make_unique — non scrivere mai unique_ptr(new T(...)).
3. Restituisci unique_ptr dalle factory — si converte implicitamente in shared_ptr se il chiamante ne ha bisogno.
4. Passa per valore per trasferire la proprietà — void sink(unique_ptr p). Il chiamante deve usare std::move.
5. Passa un puntatore grezzo o un riferimento per osservare — void use(const T& obj) oppure void use(T* obj).
6. Marca costruttore e assegnamento di spostamento come noexcept — è fondamentale per i tipi memorizzati nei contenitori.

Concetti chiave
  • unique_ptr offre la proprietà esclusiva a costo zero rispetto ai puntatori grezzi
  • Crealo sempre con std::make_unique(args...) — non usare mai new grezza
  • Non copiabile ma spostabile — la proprietà si trasferisce tramite std::move
  • I deleter personalizzati portano il RAII alle risorse C (FILE*, socket, ecc.)
  • Restituisci unique_ptr dalle funzioni factory — se serve, si converte in shared_ptr
  • Passa un puntatore grezzo o un riferimento quando una funzione si limita a osservare senza possedere

Quiz — Metti alla prova le tue conoscenze

(15 XP)

1. Qual è il costo aggiuntivo di `std::unique_ptr<T>` rispetto a un `T*` grezzo (con il deleter predefinito)?

2. Come dovresti passare un `unique_ptr` a una funzione che deve acquisirne la proprietà?

3. Perché `std::make_unique` è preferibile a `unique_ptr<T>(new T(...))`?