I puntatori intelligenti sono una funzionalità moderna del C++, pensata per offrire una gestione automatica della memoria e per aiutare a prevenire le perdite di memoria e i puntatori pendenti che si associano comunemente ai puntatori grezzi. Fungono da wrapper attorno ai puntatori grezzi, aggiungendo funzionalità come la deallocazione automatica della memoria quando il puntatore non serve più.
I puntatori intelligenti sono in genere implementati come template di classe nella libreria standard del C++.
I due puntatori intelligenti più usati sono std::unique_ptr e std::shared_ptr.
std::unique_ptr è un puntatore intelligente che possiede l'oggetto in modo esclusivo.
Garantisce che in qualsiasi momento un solo oggetto std::unique_ptr possieda la risorsa.
Quando il std::unique_ptr che la possiede viene distrutto o resettato, distrugge automaticamente gli oggetti e ne rilascia la memoria.
#include <memory>
// Declaring and defining a unique pointer
auto rightful_king_of_england = std::make_unique<std::string>("Excalibur");
// Unique pointers cannot be copied or assigned
auto mordred = rightful_king_of_england; // Error: Cannot copy a unique_ptr
std::make_unique()
Quando si crea uno std::unique_ptr, è preferibile usare std::make_unique() invece di allocare la memoria direttamente con new.
std::make_unique() offre diversi vantaggi:
std::make_unique() garantisce la sicurezza rispetto alle eccezioni.
Se viene lanciata un'eccezione durante la costruzione dell'oggetto, la memoria viene deallocata automaticamente, prevenendo perdite di memoria.std::make_unique() rende il codice più chiaro e conciso.
Elimina la necessità di specificare esplicitamente il tipo da allocare, poiché gli argomenti del template vengono dedotti automaticamente.std::make_unique() in modo più efficace rispetto all'allocazione manuale della memoria con new, con potenziali miglioramenti delle prestazioni.std::unique_ptr viene costruito manualmente.
Questo porterebbe a un comportamento indefinito, quando lo std::unique_ptr prova a eliminarla alla fine dello scope.std::shared_ptr è un puntatore intelligente che permette a più oggetti std::shared_ptr di condividere la proprietà della stessa risorsa.
Tiene traccia di quanti puntatori condivisi fanno riferimento alla risorsa e dealloca la memoria solo quando l'ultimo puntatore condiviso che la possiede esce dallo scope o viene resettato.
// Declaring and defining a shared pointer to a dynamically allocated string
auto martian_congressional_republic = std::make_shared<std::string>("protomolecule");
// Creating more shared pointer that shares ownership
auto outer_planets_alliance = martian_congressional_republic;
auto united_nations = martian_congressional_republic;
In C++17 e versioni precedenti, usare std::shared_ptr con gli array tramite std::make_shared<T[]> non è supportato direttamente.
Sebbene sia possibile allocare array con std::make_shared<T[]>, creare puntatori condivisi direttamente da essi può portare a un comportamento indefinito, a causa delle differenze nella gestione della memoria tra oggetti singoli e array.
Valuta invece di usare std::vector o funzioni di eliminazione personalizzate per gestire gli array con i puntatori condivisi in modo efficace.
Assicurati sempre della compatibilità con il tuo compilatore e con la tua implementazione della libreria standard quando hai a che fare con allocazioni di array e puntatori condivisi in C++17.
std::make_shared()
Simile a std::make_unique(), std::make_shared() offre vantaggi come una migliore efficienza della memoria, la sicurezza rispetto alle eccezioni e la leggibilità.
Combina l'allocazione della memoria per il blocco di controllo e per l'oggetto gestito in un'unica operazione, migliorando l'efficienza e riducendo il rischio di perdite di memoria.
Inoltre, la deduzione automatica degli argomenti del template semplifica il codice e ne migliora la leggibilità.
Usare std::make_shared() favorisce un codice più pulito, sicuro ed efficiente quando si lavora con oggetti std::shared_ptr in C++.
std::weak_ptr è una classe complementare a std::shared_ptr che fornisce un riferimento «debole» non proprietario a un oggetto gestito da un puntatore condiviso.
Permette di accedere alla risorsa senza influire sulla sua durata.
I puntatori deboli sono utili negli scenari in cui è necessario interrompere i riferimenti ciclici per prevenire perdite di memoria.
// Creating a shared pointer
auto your_account = std::make_shared<std::string>("secret_subscription_password");
// Creating a shared pointer that shares ownership
auto your_flatmates_account = your_account;
// Creating a weak pointer from the shared pointer
auto your_flatmates_boyfriends_account = your_flatmates_account;
// if your_account and your_flatmates_account are deleted, there is no more reference to the shared pointer.
// your_flatmates_boyfriends_account will be a null pointer and cannot use the associated object any longer.
std::weak_ptr e la proprietà ciclicastd::weak_ptr è stato progettato per risolvere il problema della proprietà ciclica, nota anche come riferimenti circolari, che può verificarsi usando std::shared_ptr.
In uno scenario di proprietà ciclica, due o più oggetti std::shared_ptr si riferiscono l'uno all'altro, creando un ciclo in cui nessuno degli oggetti può essere eliminato, perché hanno riferimenti forti reciproci, il che porta a perdite di memoria.
std::weak_ptr offre una soluzione a questo problema, permettendo riferimenti deboli a oggetti condivisi senza contribuire al loro conteggio dei riferimenti.
Ciò significa che può osservare e accedere all'oggetto condiviso, ma non ne impedisce l'eliminazione.
Se tutti i riferimenti forti all'oggetto condiviso vengono rilasciati, l'oggetto viene distrutto e i puntatori deboli che lo osservano vengono automaticamente resettati a nullptr.
Ecco un breve esempio che mostra l'uso di std::weak_ptr per interrompere una proprietà ciclica:
struct Node {
Node() { std::cout << "Node constructed\n"; }
~Node() { std::cout << "Node destructed\n"; }
std::weak_ptr<Node> next;
};
// Creating two nodes
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
// Creating a cycle by making each node point to the other
node1->next = node2;
node2->next = node1;
// Release strong references to nodes
node1.reset();
node2.reset();
// Nodes are automatically destructed due to weak references
"Node constructed" viene stampato due volte, seguito da due righe di "Node destructed".
Quando next non è un puntatore debole ma uno std::shared_ptr, si crea una situazione di proprietà ciclica che porta a perdite di memoria, perché entrambi i nodi manterranno riferimenti forti reciproci, impedendone la distruzione.
struct Node {
Node() { std::cout << "Node constructed\n"; }
~Node() { std::cout << "Node destructed\n"; }
std::shared_ptr<Node> next; // Change to shared_ptr
};
In questo scenario, sia node1 che node2 mantengono riferimenti forti reciproci tramite std::shared_ptr, formando un ciclo.
Quando il programma termina e tenta di rilasciare i riferimenti forti con reset(), i conteggi dei riferimenti di entrambi i nodi restano diversi da zero, perché ogni nodo mantiene un riferimento forte all'altro.
Di conseguenza, i nodi non vengono distrutti, il che porta a perdite di memoria.
"Node constructed" viene stampato due volte, ma non è seguito da alcuna riga di "Node destructed".
std::weak_ptr
I puntatori pendenti si verificano quando un puntatore fa riferimento a un oggetto che è stato eliminato, portando a un comportamento indefinito quando lo si dereferenzia.
std::weak_ptr aiuta a evitare i puntatori pendenti fornendo un riferimento debole e non proprietario a un oggetto gestito da std::shared_ptr.
Ecco come evitare i puntatori pendenti con std::weak_ptr:
Crea uno std::weak_ptr: invece di mantenere direttamente uno std::shared_ptr, crea uno std::weak_ptr verso l'oggetto condiviso.
Verifica la validità: prima di usare lo std::weak_ptr, verifica la sua validità con la funzione expired().
Questa funzione restituisce true se l'oggetto condiviso associato è stato eliminato.
Blocca lo std::weak_ptr: per accedere all'oggetto condiviso in sicurezza, usa la funzione lock(), che restituisce uno std::shared_ptr che punta allo stesso oggetto, se è ancora valido.
Se l'oggetto è stato eliminato, lock() restituisce uno std::shared_ptr vuoto.
Ecco un esempio di codice che mostra l'uso di std::weak_ptr per evitare i puntatori pendenti:
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
// Create weak pointers
node1->next = node2;
node2->next = node1;
// Check validity and lock
if (auto lockedNode2 = node1->next.lock()) {
// Use lockedNode2 safely
} else {
// Handle case where node2 has been deleted
}
In questo esempio, node1->next e node2->next sono oggetti std::weak_ptr.
Prima di accedere all'oggetto condiviso, se ne verifica la validità con la funzione expired().
Poi si usa lock() per accedere all'oggetto condiviso in sicurezza.
Questo approccio garantisce che i puntatori pendenti vengano evitati quando si lavora con std::weak_ptr.
Usa i puntatori intelligenti come impostazione predefinita: std::unique_ptr per la proprietà esclusiva e std::shared_ptr per la proprietà condivisa.
Riserva i puntatori grezzi ai riferimenti non proprietari o all'interfacciamento con codice legacy.
Nella maggior parte dei casi, std::unique_ptr è sufficiente per la proprietà esclusiva, perché offre una gestione della memoria leggera, senza il sovraccarico del conteggio dei riferimenti.
std::shared_ptr va usato con parsimonia, perché introduce sovraccarico e complessità, a meno che non serva una vera proprietà condivisa.
std::weak_ptr è specializzato per interrompere dipendenze cicliche o per osservare oggetti condivisi, ma non è di uso comune.