franco – Italian C++ Community https://www.italiancpp.org Mon, 24 Aug 2020 13:03:53 +0000 it-IT hourly 1 https://wordpress.org/?v=4.7.18 106700034 Puntatori? Vivi senza! https://www.italiancpp.org/2013/08/23/puntatori-vivi-senza/ https://www.italiancpp.org/2013/08/23/puntatori-vivi-senza/#comments Fri, 23 Aug 2013 08:24:56 +0000 http://www.italiancpp.org/?p=1070 Molte volte il C++, essendo un “figlio” del C, viene identificato con in puntatori. Altro non possiamo dire che “non è vero“.

Ma cominciamo con le basi. Nota bene: nel seguito utilizzeremo termini come “heap” e “stack“, anche se lo standard ISO C++ non ne fa menzione (di fatti non è detto che una piattaforma disponga di stack, ad esempio).

Tutti voi sapete che ogni variabile dichiarata in un blocco di codice, di fatti è allocata in modo automatico sullo stack, una memoria molto veloce ed estremamente limitata:


if (true)
{
int i; // Stack
i = 42;
}

Nell’esempio, la variabile i è allocata sullo stack. Ma cosa succede se dovessimo utilizzare classi molto onerose dal punto di vista dell’occupazione di memoria?

Lo stack non è più una opzione valida, perché è una memoria preziosa. Come hanno insegnato, è necessario allocare tutto sullo heap. E questo sicuramente vi fa venire in mente i puntatori:


if (true)
{
myHugeClass *p; // Stack
p = new myHugeClass(); // Heap
// ...
delete p;
}

Funziona tutto benissimo. Come si nota, il puntatore è allocato sullo stack, è dunque una variabile automatica, e la memoria è deallocata quando si esce dal blocco. Il dato, è invece allocato sullo heap dall’operatore new. Al termine dell’utilizzo della variabile p, dobbiamo ricordarci di deallocare la memoria, e questo è il compito dell’operatore delete.

C’è solo un piccolo problema: e se scordassimo il delete? Benvenuti nel tragico mondo dei memory leak.

La memoria non verrebbe mai deallocata, e la vostra applicazione allocherà nuovamente uno spazio per myHugeClass ogni volta che verrà eseguito il codice. Potenzialmente, potremmo saturare la RAM, con conseguenze catastrofiche. Come ovviare al problema? Essenzialmente dovremmo fare una accoppiata di tutti i new, con un delete, ma questo risulta impraticabile, come vedremo fra breve: non è sempre ovvio dove si trovi una deallocazione.

Smart Pointers

Lo standard ISO C++11 fornisce una soluzione elegante e semplice: utilizzare uno smart pointer, ovvero un puntatore “intelligente”, che dealloca la memoria automaticamente come se fosse sullo stack. La sintassi è estremamente semplice, e l’esempio precedente si riassume in questo codice:


if (true)
{
unique_ptr<myHugeClass> p(new myHugeClass()); // Heap
// C++14: auto p = make_unique<myHugeClass>();
// ...
}

Abbiamo risolto il problema. In questo caso, p è allocato sullo stack, mentre l’istanza di myHugeClass è allocata sullo heap, come ogni puntatore. La cosa “intelligente” degli smart pointers è che alla fine dello scope di p, tutta la memoria verrà automaticamente deallocata, sia ovviamente quello sullo stack, che quella sullo heap. Un delete non è più necessario, e dite addio al memory leak.

Solo per menzionarlo, un altro tipo di smart pointer è lo shared_ptr. Mentre lo unique_ptr consente che ci sia solo un “proprietario” dell’oggetto referenziato, lo shared_ptr permette di condividerne la proprietà:


auto ptr = make_shared<myHugeClass>();
auto ptr2 = ptr; // Istanza condivisa

La vita dell’oggetto puntato viene gestita tramite reference-counting, cioè si contano quanti shared_ptr referenziano l’oggetto puntato e quando uno shared_ptr viene distrutto è come se dicesse “io non sono più interessato alla vita dell’oggetto”. Quando l’ultimo shared_ptr viene distrutto si porta dietro anche l’oggetto puntato e il gioco è fatto!

Per chi viene dal mondo Java, potete vedere il parallelo facilmente: ogni oggetto in Java può essere pensato come uno shared_ptr (con un garbage collector). Solo che in C++ avete la possibilità di scegliere se usare un pointer o uno smart pointer.

Eccezioni

Una ulteriore motivazione sull’uso degli smart pointers al posto dei raw pointers, è nel caso di eccezioni. Prendiamo il secondo esempio. Se allochiamo con new una istanza di myHugeClass, ed il costruttore va in eccezione? A questo punto il codice è inutilizzabile, perché p punta ad una zona di memoria non valida. Cosa dovremmo fare per evitare la catastrofe di utilizzare un pointer invalido? Un semplice trucco consisterebbe nell’uso di try/catch:


if (true)
{
myHugeClass *p = nullptr; // Stack
try
{
p = new myHugeClass(); // Heap
// ...
delete p; // Rilascio la memoria: ho terminato
}
catch(...)
{
delete p; // Rilascio la memoria: errore rilevato
}
}

Sembra semplice, ma questo ci fa venire in mente il problema dell’accoppiamento new con delete. Mentre prima potevamo contare i new, e controllare che il numero di delete fosse uguale, ora non è più valida questa soluzione: abbiamo due deallocazioni, una per un funzionamento fisiologico, una per quello patologico con eccezioni. E la cosa si complica notevolmente con molte variabili e più modi di gestire varie eccezioni.

E dunque gli smart pointers ci aiutano? Certamente: è garantito dallo standard che, nel caso in cui una eccezione venga lanciata, la memoria debba essere automaticamente deallocata. Ecco come diventa l’esempio di prima:


if (true)
{
auto p = make_unique<myHugeClass>(); // C++14 style
} // Il delete è automatico

Conclusioni

Gli smart pointers sono utili classi da utilizzare sempre, ove possibile. Certo è che non è sempre praticabile l’uso degli smart pointers, alcune volte serviranno i cari vecchi raw pointers, ma sono casi particolari. In genere, utilizzare un raw pointer è sconsigliato.

Ma le buone notizie non terminano con questo. Se utilizzate ad esempio i containers, come vector, l’implementazione garantisce che la variabile sia sullo stack, mentre i dati siano allocati dinamicamente sullo heap. E questo non vale ovviamente solo per vector!

Ancora più interessante è il caso in cui voi vogliate usare una funzione che come valore di ritorno ha una istanza molto grande, come ad esempio un vector con molti elementi. Potreste pensare che, associando una variabile al valore di ritorno della vostra funzione, venga copiato ogni elemento dentro il vettore, con chiamate a non finire al costruttore dell’oggetto contenuto nel vector (ad esempio un vettore di myHugeClass): questo sarebbe un overhead enorme. In realtà, dipendentemente dal compilatore però, il C++ fornisce una soluzione automatica, non copiando l’elemento, ma muovendolo, con una tecnica semplice chiamata return value optimization. Ma questa, è un’altra storia.

]]>
https://www.italiancpp.org/2013/08/23/puntatori-vivi-senza/feed/ 24 1070