RAII ed eccezioni
With the advent of smart pointers, is it a sign of poor design if I
see objects are deleted?
Dipende dal contesto. Diamo un'occhiata a un esempio come questo:
Foo* foo = new Foo;
Bar* bar = new Bar;
func(foo, bar);
delete foo;
delete bar;
* Nota: non utilizzare lo stack in questo esempio è sciocco. Immaginiamo nel mondo reale che Foo e Bar siano dei pimpl, o che siano in definitiva tipi astratti da memorizzare / possedere in una struttura di dati polimorfici, siano creati attraverso una fabbrica, vengano distrutti solo quando l'utente richiede di scaricare la risorsa, ecc.
C'è qualcosa di sbagliato in questo codice? Alcune persone potrebbero pensare "no". Tranne che ci sono tutti i tipi di problemi con questo codice, e non si riferiscono affatto a pratiche o stile - voglio dire che questo codice è buggy . Il problema è questo:
Foo* foo = new Foo; // this can throw
Bar* bar = new Bar; // this can throw
func(foo, bar); // this might throw
delete foo;
delete bar;
È l'interazione del rilascio manuale delle risorse con la gestione delle eccezioni che rende i distruttori che automatizzano questo spesso una necessità assoluta (vedi RAII). Per fare in modo che il suddetto codice non sia più bacato da percorsi eccezionali senza raggiungere i distruttori per automatizzarlo, potremmo aver bisogno di fare qualcosa del genere:
Foo* foo = new Foo; // this can throw
try
{
Bar* bar = new Bar; // this can throw
try
{
func(foo, bar); // this might throw
}
catch (...)
{
delete bar;
throw;
}
delete bar;
}
catch (...)
{
delete foo;
throw;
}
delete foo;
... phew! Questo dovrebbe, se non ho commesso errori (il che è ancora molto possibile), correggere gli errori. Un altro modo è questo:
Foo* foo = new Foo; // this can throw
Bar* bar = 0;
try
{
bar = new Bar; // this can throw
func(foo, bar); // this might throw
}
catch (...)
{
delete foo;
delete bar;
throw;
}
delete foo;
delete bar;
... che potrebbe essere un po 'più semplice, ma comunque piuttosto complicato (e immagina quanto sia facile incasinare tutto se siamo tornati e abbiamo apportato delle modifiche in seguito, o se uno dei nostri compagni di squadra l'ha fatto).
Ora consideriamo come possiamo correggere i bug usando i puntatori intelligenti:
unique_ptr<Foo> foo(new Foo);
unique_ptr<Bar> bar(new Bar);
func(foo.get(), bar.get());
Tada! Non so voi, ma sembra molto più facile da leggere, scrivere, gestire. Attraverso i distruttori che rilasciano automaticamente le risorse, finiamo per rendere le nostre vite così, così, molto più semplici.
Exception-Sicurezza
Alcune persone potrebbero pensare che l'esempio sopra sia un po 'discutibile, dal momento che sta correggendo bug in eccezionali percorsi di esecuzione. Almeno per i programmi giocattolo, preoccuparsi del comportamento corretto di fronte a un'eccezione potrebbe essere eccessivo.
Tuttavia, per il codice di produzione, lavoro in un'area che non è molto critica per la missione, ma le persone legano ancora il loro sostentamento professionale (non vite) al software. In questi casi, in questo caso, deve prestare attenzione alla sicurezza delle eccezioni, per consentire al software di ripristinarsi con garbo in caso di eccezioni (e anche scrivere test che simulino percorsi di eccezione lanciando mentre si controllano i rilevatori di fughe e simili). I distruttori che rilasciano automaticamente le risorse e / o ripristinano gli effetti collaterali sono un salvagente qui quando si interagisce con le eccezioni.
Puntatori intelligenti
Detto questo, dobbiamo usare puntatori intelligenti ovunque, di per sé? Qui offrirò una risposta distorta che potrebbe risultare inusuale in quanto si lavora su aree di livello molto basso e ad alte prestazioni.
Non c'è alcun costo per qualcosa come std::unique_ptr
a condizione che non ci sia bisogno di un deleter personalizzato. Ma spesso in queste aree in cui lavoro di più, usiamo allocatori personalizzati, richiedendo blocchi da un pool di memoria e restituendoli quando abbiamo finito. Spesso il mio codice non assomiglia a questo:
Foo* foo = new Foo;
...
delete foo;
Invece sembra così usando il nuovo posizionamento:
Foo* foo = new(allocator.allocate()) Foo;
...
foo->~Foo();
allocator.deallocate(foo);
Con questi tipi di casi, può risultare un po 'difficile utilizzare puntatori intelligenti. Quelli standard tendono ad avere un sovraccarico quando sono coinvolti delet personalizzati che vanifica alcuni degli scopi dell'uso di allocatori fissi e tali da migliorare la localizzazione di riferimento (un numero minore di puntatori intelligenti che si inseriscono in una linea della cache). Inoltre, il codice diventa un po 'più complicato.
In questi casi è impossibile persino lanciare i miei puntatori intelligenti senza pagare un costo di runtime, dal momento che dovrebbero essere iniettati con l'allocatore personalizzato che sto usando su singoli oggetti, e questo sarebbe almeno equivalente a raddoppiando la dimensione del puntatore intelligente (lavoro in aree come strutture di dati del grafico dove la rasatura di 8 byte per istanza può fare la differenza tra 1,2 gigabyte di memoria e un gigabyte, ad esempio).
Detto questo, questa non è una scusa per evitare RAII. Quindi quello che faccio è molto attentamente la memoria libera nei miei distruttori (per intere strutture di dati, non per ogni elemento all'interno), per avvolgere questo tipo di codice in oggetti che gestiscono la memoria attraverso il loro costruttore e distruttore, e comunque beneficiare di RAII in quel modo . La gestione delle risorse è ancora automatizzata per coloro che usano le mie classi, devo solo stare attento e implementare i miei distruttori con la gestione della memoria manuale coinvolta e scrivere test per assicurarmi di non dimenticare di liberare memoria in un distruttore.
Questo è un caso eccezionale: per la maggior parte delle persone, direi che è abbastanza saggio usare semplicemente puntatori intelligenti il più possibile per i puntatori che possiedono memoria se la strategia alternativa sta usando il default, lanciando operator new
e operator delete
. È solo quando sono coinvolti gli allocatori personalizzati che le cose diventano un po 'torbide.