RAII e smart pointer in C++
La gestione manuale della memoria con new e delete è una delle principali fonti di errori nei programmi C++. Per affrontare il problema in modo sistematico, il linguaggio adotta un principio noto come RAII, acronimo di Resource Acquisition Is Initialization, ovvero acquisizione della risorsa al momento dell'inizializzazione. Su questo principio si fondano gli smart pointer, oggetti che gestiscono automaticamente il ciclo di vita della memoria dinamica e che oggi rappresentano la pratica raccomandata.
Il principio RAII
L'idea alla base di RAII è semplice ma profonda: una risorsa, come memoria, un file o una connessione di rete, viene acquisita nel costruttore di un oggetto e rilasciata nel suo distruttore. Poiché il distruttore viene invocato automaticamente quando l'oggetto esce dall'ambito, la risorsa viene liberata senza bisogno di intervento esplicito, anche in presenza di errori o eccezioni. Il ciclo di vita della risorsa viene così legato al ciclo di vita di un oggetto sullo stack.
#include <iostream>
// Esempio didattico del principio RAII
class FileGuard {
public:
FileGuard() {
std::cout << "File aperto" << std::endl;
}
~FileGuard() {
// Rilascio garantito all'uscita dall'ambito
std::cout << "File chiuso" << std::endl;
}
};
int main() {
FileGuard guard; // Apertura
std::cout << "Uso del file" << std::endl;
return 0; // Qui il distruttore chiude automaticamente il file
}
std::unique_ptr
Il primo smart pointer da conoscere è std::unique_ptr, definito nell'intestazione memory. Rappresenta la proprietà esclusiva di un oggetto allocato dinamicamente: un solo unique_ptr per volta possiede la risorsa, e quando viene distrutto libera automaticamente la memoria. Per crearlo si usa la funzione std::make_unique, che è il modo più sicuro ed efficiente.
#include <memory>
#include <iostream>
int main() {
// Proprieta esclusiva: nessun delete manuale
std::unique_ptr<int> number = std::make_unique<int>(42);
std::cout << *number << std::endl;
return 0; // La memoria viene liberata automaticamente
}
Poiché la proprietà è esclusiva, un unique_ptr non può essere copiato, ma può essere trasferito a un altro unique_ptr tramite spostamento, con la funzione std::move. Dopo lo spostamento, il puntatore di origine non possiede più la risorsa.
std::unique_ptr<int> first = std::make_unique<int>(10);
// Trasferimento della proprieta a second
std::unique_ptr<int> second = std::move(first);
// Ora first non possiede piu nulla
std::shared_ptr
Quando più parti del programma devono condividere la proprietà dello stesso oggetto, si usa std::shared_ptr. Questo smart pointer mantiene un conteggio dei riferimenti: ogni copia incrementa il contatore, ogni distruzione lo decrementa, e la memoria viene liberata solo quando l'ultimo shared_ptr che la possiede viene distrutto. Si crea con std::make_shared.
#include <memory>
#include <iostream>
int main() {
std::shared_ptr<int> first = std::make_shared<int>(99);
{
std::shared_ptr<int> second = first; // Conteggio: 2
std::cout << first.use_count() << std::endl;
} // second esce dall'ambito, conteggio: 1
std::cout << first.use_count() << std::endl;
return 0;
}
Il problema dei riferimenti circolari
I shared_ptr presentano un'insidia: se due oggetti si riferiscono reciprocamente tramite shared_ptr, il conteggio dei riferimenti non raggiunge mai lo zero e la memoria non viene mai liberata. Per spezzare questi cicli si utilizza std::weak_ptr, un puntatore che osserva una risorsa senza parteciparne alla proprietà e quindi senza incrementare il conteggio dei riferimenti.
Quando scegliere quale
La regola pratica del C++ moderno è chiara: si preferisce sempre std::unique_ptr come scelta predefinita, perché esprime la proprietà esclusiva con il minimo costo. Si ricorre a std::shared_ptr solo quando la proprietà deve essere effettivamente condivisa, e a std::weak_ptr per rompere eventuali cicli. L'uso diretto di new e delete nel codice applicativo va evitato. Affidandosi agli smart pointer e al principio RAII si eliminano alla radice intere categorie di errori legati alla memoria, scrivendo codice più sicuro e più facile da mantenere.