Il problema che risolvono gli smart pointer
Nella pagina precedente hai allocato memoria con new e l'hai liberata con delete. Funziona, ma il peso ricade su di te: ogni new richiede un delete corrispondente, su ogni percorso del codice, compresi quelli in cui viene lanciata un'eccezione a metà strada. Ne dimentichi uno e hai un memory leak; esegui delete due volte e corrompi l'heap.
Gli smart pointer risolvono il problema legando la durata della memoria dell'heap a un normale oggetto sullo stack. Quando quell'oggetto esce dallo scope, il suo distruttore esegue delete al posto tuo, con la garanzia che succeda anche se un'eccezione srotola lo stack. Quest'idea si chiama RAII (Resource Acquisition Is Initialization), e gli smart pointer si trovano nell'header <memory>.
Usi *p e p->member esattamente come faresti con un puntatore grezzo. La differenza è che non chiami mai delete: ci pensa lo smart pointer.
unique_ptr: un solo proprietario, nessuna condivisione
unique_ptr è lo smart pointer da usare di default. Rappresenta una proprietà esclusiva: esattamente un unique_ptr possiede l'oggetto alla volta, e quando quel puntatore muore l'oggetto muore con lui. Rispetto a un puntatore grezzo non ha alcun costo aggiuntivo a runtime.
Creane uno con make_unique (C++14). Riceve gli argomenti del costruttore e ti consegna un puntatore pronto all'uso:
Poiché può esserci un solo proprietario, un unique_ptr non si può copiare. Provare a copiarlo è un errore di compilazione, ed è il linguaggio che ti protegge dall'avere due proprietari che cercano entrambi di fare delete dello stesso oggetto:
auto a = make_unique<int>(10);
auto b = a; // error: call to deleted copy constructor of unique_ptr
Per cedere la proprietà a qualcun altro, la sposti con std::move. Dopo lo spostamento, il puntatore originale è vuoto (contiene nullptr):
È il modello che vuoi nella maggior parte dei casi: c'è sempre esattamente un proprietario chiaro, e il compilatore lo fa rispettare.
shared_ptr: proprietà condivisa con il conteggio dei riferimenti
A volte più parti del programma hanno davvero bisogno di condividere lo stesso oggetto, e nessuna sa quale finirà per ultima. È a questo che serve shared_ptr. Tiene un conteggio dei riferimenti: ogni copia lo incrementa, ogni distruzione lo decrementa, e l'oggetto viene liberato solo quando il conteggio arriva a zero.
Creali con make_shared:
A differenza di unique_ptr, copiare uno shared_ptr va benissimo: è proprio questo il punto. Il prezzo è il costo: il conteggio dei riferimenti è memorizzato sull'heap e aggiornato in modo atomico (thread-safe), quindi shared_ptr è più pesante di unique_ptr. Usalo solo quando la proprietà è davvero condivisa, non per evitare di pensare a chi possiede cosa.
make_shared è anche più efficiente di shared_ptr<T>(new T(...)): alloca l'oggetto e il blocco di controllo in una sola allocazione invece che in due.
weak_ptr e come spezzare i cicli di riferimenti
shared_ptr ha una trappola classica: se due oggetti si tengono a vicenda tramite shared_ptr, i loro conteggi dei riferimenti non arrivano mai a zero, quindi nessuno dei due viene mai liberato. È un memory leak anche se hai usato gli smart pointer.
struct Node {
shared_ptr<Node> next; // se due nodi puntano l'uno all'altro,
}; // si tengono in vita a vicenda per sempre
La soluzione è weak_ptr: un osservatore non proprietario di uno shared_ptr. Non incrementa il conteggio dei riferimenti, quindi non tiene mai in vita un oggetto. Per usare l'oggetto chiami .lock(), che ti dà uno shared_ptr se l'oggetto esiste ancora, o uno vuoto se è già sparito.
Usa weak_ptr per i "puntatori all'indietro" e per le cache: ovunque tu voglia riferirti a un oggetto senza rivendicarne la proprietà.
Errori comuni e trappole
Gli smart pointer eliminano la maggior parte dei bug di memoria, ma restano alcune trappole:
Non mescolare proprietà smart e grezza della stessa memoria. Non costruire mai due smart pointer dallo stesso puntatore grezzo: ognuno cercherà di farne delete:
int* raw = new int(5);
unique_ptr<int> a(raw);
unique_ptr<int> b(raw); // disastro: entrambi faranno delete dello stesso int (double free)
È proprio per questo che preferisci make_unique/make_shared: non c'è alcun puntatore grezzo in giro da usare male.
Un unique_ptr si può solo spostare, quindi passalo per valore per trasferire la proprietà. Se una funzione deve usare l'oggetto ma non possederlo, ricevi invece un semplice riferimento o un T* grezzo: un puntatore grezzo che si limita a osservare va benissimo:
void consume(unique_ptr<int> p); // prende la proprietà (ci si sposta dentro)
void observe(int* p); // guarda soltanto, non possiede nulla
Non usare shared_ptr di default. È allettante perché si copia liberamente, ma il conteggio atomico dei riferimenti ha un costo reale in prestazioni, e la proprietà condivisa rende più difficile ragionare sulla durata degli oggetti. Usa di default unique_ptr; passa a shared_ptr solo quando ti servono davvero più proprietari.
unique_ptr per gli array richiede la forma per array. make_unique<int[]>(n) ti dà un unique_ptr<int[]> che chiama correttamente delete[]. In pratica, per gli array dinamici preferisci std::vector: gestisce la memoria al posto tuo e in più tiene traccia della dimensione.
Prossimo passo: le stringhe
Ora hai la gestione della memoria sotto controllo: gli smart pointer ti danno l'allocazione sull'heap senza memory leak. Una delle cose che allocherai e passerai in giro più spesso è il testo, e C++ ti offre uno strumento molto più sicuro dei buffer char* grezzi. La prossima pagina tratta std::string: come cresce da sola, le operazioni che userai ogni giorno e perché ti libera del tutto dalla gestione manuale della memoria.
Domande frequenti
Cosa sono gli smart pointer in C++?
Gli smart pointer sono oggetti di <memory> (unique_ptr, shared_ptr, weak_ptr) che avvolgono un puntatore grezzo e fanno automaticamente delete della memoria quando escono dallo scope. Ti danno l'allocazione sull'heap senza il delete manuale e senza i memory leak che arrivano quando te lo dimentichi.
Qual è la differenza tra unique_ptr e shared_ptr?
unique_ptr è l'unico proprietario del suo oggetto: non si può copiare, solo spostare, e libera la memoria nel momento in cui muore. shared_ptr permette una proprietà condivisa tramite il conteggio dei riferimenti: molti shared_ptr possono puntare allo stesso oggetto, che viene liberato solo quando viene distrutto l'ultimo. Preferisci unique_ptr, a meno che non ti serva davvero una proprietà condivisa.
Nel C++ moderno conviene usare make_unique o new?
Usa make_unique e make_shared. Allocano l'oggetto e lo avvolgono in un solo passaggio, quindi non c'è alcun new grezzo il cui risultato potrebbe andare perso prima di arrivare a uno smart pointer. Come regola pratica, una codebase C++ moderna non dovrebbe contenere quasi nessun new o delete esplicito.