Vai al contenuto

shared_ptr, weak_ptr e grafi di proprietà

Comprendi la proprietà condivisa con conteggio dei riferimenti tramite shared_ptr, spezza i cicli con weak_ptr, impara che cosa sono il blocco di controllo e l'aliasing e riconosci quando shared_ptr è la scelta sbagliata.

shared_ptr: proprietà condivisa

std::shared_ptr abilita la proprietà condivisa — più istanze di shared_ptr possono possedere lo stesso oggetto. L'oggetto viene distrutto quando l'ultimo shared_ptr proprietario viene distrutto o reimpostato.

Internamente, shared_ptr mantiene un blocco di controllo che contiene:
- Un conteggio dei riferimenti forti (numero di proprietari shared_ptr)
- Un conteggio dei riferimenti deboli (numero di osservatori weak_ptr)
- Il deleter e l'allocatore

shared_ptr è più grande di un puntatore grezzo (tipicamente 2 puntatori: uno all'oggetto, uno al blocco di controllo) e il conteggio dei riferimenti comporta un costo di operazioni atomiche a ogni copia e a ogni distruzione. Usalo solo quando la proprietà è realmente condivisa.

make_shared e il conteggio dei riferimenti

std::make_shared(args...) alloca l'oggetto E il blocco di controllo con una singola allocazione (anziché due). Questo è più efficiente per la cache ed evita il problema di sicurezza rispetto alle eccezioni della new grezza.

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

struct Logger {
    std::string name;
    Logger(std::string n) : name(std::move(n)) {
        std::cout << name << " created\n";
    }
    ~Logger() { std::cout << name << " destroyed\n"; }
    void log(const std::string& msg) {
        std::cout << "[" << name << "] " << msg << '\n';
    }
};

int main() {
    auto logger = std::make_shared<Logger>("AppLogger");
    std::cout << "use_count: " << logger.use_count() << '\n';  // 1

    {
        auto copy = logger;  // shared ownership
        std::cout << "use_count: " << logger.use_count() << '\n';  // 2
        copy->log("inside scope");
    }  // copy destroyed, count drops to 1

    std::cout << "use_count: " << logger.use_count() << '\n';  // 1
    logger->log("still alive");
}  // logger destroyed — count drops to 0, Logger deleted

weak_ptr: spezzare i cicli

std::weak_ptr osserva un oggetto gestito da shared_ptr senza estenderne la durata di vita. Per accedere all'oggetto chiama lock(), che restituisce uno shared_ptr — oppure nullptr se l'oggetto è già stato distrutto. weak_ptr è essenziale per spezzare i riferimenti circolari, che altrimenti causerebbero perdite di memoria.

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

struct Employee;
struct Team {
    std::string name;
    std::vector<std::shared_ptr<Employee>> members;
    Team(std::string n) : name(std::move(n)) {}
    ~Team() { std::cout << "Team " << name << " destroyed\n"; }
};

struct Employee {
    std::string name;
    std::weak_ptr<Team> team;  // weak — does NOT own the team
    Employee(std::string n) : name(std::move(n)) {}
    ~Employee() { std::cout << "Employee " << name << " destroyed\n"; }

    void print_team() {
        if (auto t = team.lock()) {  // try to get shared_ptr
            std::cout << name << " belongs to " << t->name << '\n';
        } else {
            std::cout << name << " has no team\n";
        }
    }
};

int main() {
    auto team = std::make_shared<Team>("Engineering");
    auto alice = std::make_shared<Employee>("Alice");
    auto bob = std::make_shared<Employee>("Bob");

    team->members.push_back(alice);
    team->members.push_back(bob);
    alice->team = team;  // weak_ptr — no circular ownership
    bob->team = team;

    alice->print_team();  // "Alice belongs to Engineering"

    team.reset();  // destroy team
    alice->print_team();  // "Alice has no team" — weak_ptr expired
}
Insidia

Se due oggetti si tengono a vicenda tramite shared_ptr, nessuno dei due potrà mai raggiungere un conteggio dei riferimenti pari a zero — si mantengono in vita l'un l'altro per sempre. Questa è una perdita di memoria, anche se stai usando gli smart pointer.

struct Node {
    std::shared_ptr<Node> next;  // se next punta all'indietro, PERDITA!
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->next = a;  // circolare — né a né b verranno mai distrutti

La soluzione: rendi una delle due direzioni un weak_ptr. Nelle strutture ad albero i figli usano un weak_ptr verso il genitore (o puntatori osservatori grezzi). Nelle liste doppiamente concatenate, usa un weak_ptr per il puntatore prev.

Il costruttore di aliasing ed enable_shared_from_this

Il costruttore di aliasing crea uno shared_ptr che condivide la proprietà con uno shared_ptr esistente ma punta a un oggetto diverso (tipicamente un membro). L'oggetto originale resta in vita finché esiste almeno uno shared_ptr in aliasing:

struct Pair { int first; int second; };
auto p = std::make_shared<Pair>(Pair{1, 2});
std::shared_ptr<int> sp_first(p, &p->first);  // condivide il blocco di controllo di p
// p può uscire dallo scope — Pair resta in vita perché sp_first lo referenzia ancora

enable_shared_from_this è una classe base CRTP che permette a un oggetto gestito da shared_ptr di ottenere uno shared_ptr a se stesso tramite shared_from_this(). Senza di essa, costruire un nuovo shared_ptr a partire da this crea un secondo blocco di controllo, causando un doppio rilascio.

Buona pratica

shared_ptr è usato troppo. Ricorri a esso solo quando la proprietà è realmente condivisa tra durate di vita indipendenti. NON usare shared_ptr come scorciatoia per evitare di ragionare sulla proprietà.

Evita shared_ptr quando:
- Esiste un unico proprietario — usa invece unique_ptr (costo zero).
- L'oggetto vive sullo stack — usa semplicemente l'oggetto direttamente.
- Ti serve solo passare un riferimento — passa T& oppure T*.
- Lo stai usando "perché è sicuro" — shared_ptr ha un costo a runtime (conteggio atomico, allocazione aggiuntiva) e nasconde la semantica della proprietà.
- Sei su un percorso critico per le prestazioni — le operazioni atomiche sul conteggio dei riferimenti sono sorprendentemente costose in caso di contesa.

Scenari legittimi di proprietà condivisa: le cache (più ricerche restituiscono lo stesso oggetto), i sistemi publish/subscribe, gli oggetti di configurazione condivisi e le strutture dati a grafo con nodi realmente condivisi.

Dentro il blocco di controllo

Capire il blocco di controllo aiuta a spiegare i costi di shared_ptr. make_shared unisce l'oggetto e il blocco di controllo in un'unica allocazione. Costruirlo da una new grezza richiede invece due allocazioni separate.

control_block.cpp
#include <memory>
#include <iostream>

struct Widget {
    int data[4];
};

int main() {
    // Two allocations: one for Widget, one for control block
    std::shared_ptr<Widget> p1(new Widget{});

    // Single allocation: Widget + control block together
    auto p2 = std::make_shared<Widget>();

    // Sizes (typical 64-bit platform)
    std::cout << "sizeof(Widget*):          " << sizeof(Widget*) << '\n';   // 8
    std::cout << "sizeof(unique_ptr<Widget>): " << sizeof(std::unique_ptr<Widget>) << '\n';  // 8
    std::cout << "sizeof(shared_ptr<Widget>): " << sizeof(std::shared_ptr<Widget>) << '\n';  // 16

    // shared_ptr is 2 pointers: one to object, one to control block
    // Control block contains: strong count, weak count, deleter, allocator

    std::cout << "p2 use_count: " << p2.use_count() << '\n';  // 1
    std::weak_ptr<Widget> w = p2;
    std::cout << "p2 use_count: " << p2.use_count() << '\n';  // still 1
    // weak_ptr does NOT increase strong count
}
Concetti chiave
  • shared_ptr usa il conteggio dei riferimenti — l'oggetto muore quando sparisce l'ultimo proprietario
  • make_shared alloca oggetto + blocco di controllo con un'unica allocazione (preferiscilo sempre)
  • weak_ptr osserva senza possedere — usa lock() per accedervi in sicurezza, spezza i riferimenti circolari
  • shared_ptr è largo 2 puntatori e comporta il costo del conteggio atomico — non è gratuito
  • Usa unique_ptr come impostazione predefinita; ricorri a shared_ptr solo quando la proprietà è realmente condivisa
  • Non costruire mai due shared_ptr dallo stesso puntatore grezzo — usa enable_shared_from_this per i riferimenti a se stessi

Quiz — Metti alla prova le tue conoscenze

(15 XP)

1. Che cosa succede quando due oggetti si tengono a vicenda tramite `shared_ptr`?

2. Perché `std::make_shared` è preferibile a `shared_ptr<T>(new T(...))`?

3. Come si accede in sicurezza a un oggetto tramite un `weak_ptr`?