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 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.
#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 deletedweak_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.
#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
}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 distruttiLa 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 ancoraenable_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.
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.
#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
}shared_ptrusa il conteggio dei riferimenti — l'oggetto muore quando sparisce l'ultimo proprietariomake_sharedalloca oggetto + blocco di controllo con un'unica allocazione (preferiscilo sempre)weak_ptrosserva senza possedere — usalock()per accedervi in sicurezza, spezza i riferimenti circolarishared_ptrè largo 2 puntatori e comporta il costo del conteggio atomico — non è gratuito- Usa
unique_ptrcome impostazione predefinita; ricorri ashared_ptrsolo quando la proprietà è realmente condivisa - Non costruire mai due
shared_ptrdallo stesso puntatore grezzo — usaenable_shared_from_thisper 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`?