In pratica con C ++, che cos'è RAII , quali sono i puntatori intelligenti , come vengono implementati in un programma e quali sono i vantaggi dell'utilizzo di RAII con i puntatori intelligenti?
In pratica con C ++, che cos'è RAII , quali sono i puntatori intelligenti , come vengono implementati in un programma e quali sono i vantaggi dell'utilizzo di RAII con i puntatori intelligenti?
Risposte:
Un semplice (e forse abusato) esempio di RAII è una classe File. Senza RAII, il codice potrebbe assomigliare a questo:
File file("/path/to/file");
// Do stuff with file
file.close();
In altre parole, dobbiamo assicurarci di chiudere il file una volta terminato. Ciò ha due inconvenienti: in primo luogo, ovunque utilizziamo File, dovremo chiamare File :: close () - se ci dimentichiamo di farlo, tratteniamo il file più a lungo del necessario. Il secondo problema è cosa succede se viene generata un'eccezione prima di chiudere il file?
Java risolve il secondo problema usando una clausola finally:
try {
File file = new File("/path/to/file");
// Do stuff with file
} finally {
file.close();
}
o da Java 7, un'istruzione try-with-resource:
try (File file = new File("/path/to/file")) {
// Do stuff with file
}
C ++ risolve entrambi i problemi utilizzando RAII, ovvero chiudendo il file nel distruttore di File. Finché l'oggetto File viene distrutto al momento giusto (che dovrebbe essere comunque), la chiusura del file viene curata per noi. Quindi, il nostro codice ora assomiglia a:
File file("/path/to/file");
// Do stuff with file
// No need to close it - destructor will do that for us
Questo non può essere fatto in Java poiché non vi è alcuna garanzia quando l'oggetto verrà distrutto, quindi non possiamo garantire quando verrà liberata una risorsa come un file.
Su puntatori intelligenti: per la maggior parte del tempo, creiamo semplicemente oggetti nello stack. Ad esempio (e rubare un esempio da un'altra risposta):
void foo() {
std::string str;
// Do cool things to or using str
}
Funziona bene, ma cosa succede se vogliamo restituire str? Potremmo scrivere questo:
std::string foo() {
std::string str;
// Do cool things to or using str
return str;
}
Quindi, cosa c'è che non va? Bene, il tipo restituito è std :: string - quindi significa che stiamo tornando per valore. Ciò significa che copiamo str e in realtà restituiamo la copia. Questo può essere costoso e potremmo voler evitare il costo di copiarlo. Pertanto, potremmo venire con l'idea di tornare per riferimento o per puntatore.
std::string* foo() {
std::string str;
// Do cool things to or using str
return &str;
}
Sfortunatamente, questo codice non funziona. Restituiamo un puntatore a str - ma str è stato creato nello stack, quindi veniamo cancellati una volta usciti da foo (). In altre parole, quando il chiamante ottiene il puntatore, è inutile (e probabilmente peggio che inutile poiché usarlo potrebbe causare ogni sorta di errori funky)
Allora, qual è la soluzione? Potremmo creare str sull'heap usando new - in questo modo, quando foo () sarà completato, str non verrà distrutto.
std::string* foo() {
std::string* str = new std::string();
// Do cool things to or using str
return str;
}
Naturalmente, questa soluzione non è neanche perfetta. Il motivo è che abbiamo creato str, ma non lo eliminiamo mai. Questo potrebbe non essere un problema in un programma molto piccolo, ma in generale, vogliamo assicurarci di eliminarlo. Potremmo solo dire che il chiamante deve eliminare l'oggetto una volta che ha finito con esso. Il rovescio della medaglia è che il chiamante deve gestire la memoria, il che aggiunge ulteriore complessità e potrebbe sbagliarla, portando a una perdita di memoria, cioè non eliminando l'oggetto anche se non è più necessario.
È qui che entrano in gioco i puntatori intelligenti. L'esempio seguente usa shared_ptr - ti suggerisco di guardare i diversi tipi di puntatori intelligenti per imparare cosa vuoi veramente usare.
shared_ptr<std::string> foo() {
shared_ptr<std::string> str = new std::string();
// Do cool things to or using str
return str;
}
Ora, shared_ptr conterà il numero di riferimenti a str. Per esempio
shared_ptr<std::string> str = foo();
shared_ptr<std::string> str2 = str;
Ora ci sono due riferimenti alla stessa stringa. Una volta che non ci sono più riferimenti a str, questo verrà eliminato. Pertanto, non devi più preoccuparti di eliminarlo da solo.
Modifica rapida: come hanno sottolineato alcuni commenti, questo esempio non è perfetto per (almeno!) Due motivi. Innanzitutto, a causa dell'implementazione delle stringhe, la copia di una stringa tende ad essere poco costosa. In secondo luogo, a causa della cosiddetta ottimizzazione del valore restituito, il ritorno in base al valore potrebbe non essere costoso poiché il compilatore può fare un po 'di intelligenza per accelerare le cose.
Quindi, proviamo un esempio diverso usando la nostra classe File.
Supponiamo di voler utilizzare un file come registro. Questo significa che vogliamo aprire il nostro file in modalità solo accodamento:
File file("/path/to/file", File::append);
// The exact semantics of this aren't really important,
// just that we've got a file to be used as a log
Ora, impostiamo il nostro file come registro per un paio di altri oggetti:
void setLog(const Foo & foo, const Bar & bar) {
File file("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Sfortunatamente, questo esempio termina in modo orribile - il file verrà chiuso non appena questo metodo termina, il che significa che foo e bar ora hanno un file di registro non valido. Potremmo costruire un file sull'heap e passare un puntatore al file sia a pippo che a barra:
void setLog(const Foo & foo, const Bar & bar) {
File* file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Ma allora chi è responsabile dell'eliminazione del file? Se nessuno dei due elimina il file, abbiamo una perdita di memoria e di risorse. Non sappiamo se foo o bar finiranno per primi con il file, quindi non possiamo aspettarci di eliminare il file da soli. Ad esempio, se foo elimina il file prima che la barra abbia terminato, la barra ora ha un puntatore non valido.
Quindi, come avrete intuito, potremmo usare i puntatori intelligenti per aiutarci.
void setLog(const Foo & foo, const Bar & bar) {
shared_ptr<File> file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Ora, nessuno deve preoccuparsi di eliminare il file - una volta che sia foo che bar hanno terminato e non hanno più riferimenti al file (probabilmente a causa della distruzione di foo e bar), il file verrà automaticamente eliminato.
RAII Questo è un nome strano per un concetto semplice ma fantastico. Migliore è il nome Scope Bound Resource Management (SBRM). L'idea è che spesso ti capita di allocare risorse all'inizio di un blocco e devi rilasciarlo all'uscita di un blocco. L'uscita dal blocco può avvenire tramite il normale controllo del flusso, saltando fuori da esso e persino da un'eccezione. Per coprire tutti questi casi, il codice diventa più complicato e ridondante.
Solo un esempio farlo senza SBRM:
void o_really() {
resource * r = allocate_resource();
try {
// something, which could throw. ...
} catch(...) {
deallocate_resource(r);
throw;
}
if(...) { return; } // oops, forgot to deallocate
deallocate_resource(r);
}
Come vedi ci sono molti modi in cui possiamo essere promossi. L'idea è che incapsuliamo la gestione delle risorse in una classe. L'inizializzazione del suo oggetto acquisisce la risorsa ("L'acquisizione delle risorse è inizializzazione"). Al momento in cui usciamo dal blocco (ambito del blocco), la risorsa viene liberata di nuovo.
struct resource_holder {
resource_holder() {
r = allocate_resource();
}
~resource_holder() {
deallocate_resource(r);
}
resource * r;
};
void o_really() {
resource_holder r;
// something, which could throw. ...
if(...) { return; }
}
È bello se hai classi proprie che non sono solo allo scopo di allocare / deallocare risorse. L'assegnazione sarebbe solo una preoccupazione in più per svolgere il proprio lavoro. Ma non appena si desidera allocare / deallocare risorse, quanto sopra diventa invariato. Devi scrivere una classe di wrapping per ogni tipo di risorsa acquisita. Per facilitare ciò, i puntatori intelligenti consentono di automatizzare tale processo:
shared_ptr<Entry> create_entry(Parameters p) {
shared_ptr<Entry> e(Entry::createEntry(p), &Entry::freeEntry);
return e;
}
Normalmente, i puntatori intelligenti sono sottili involucri intorno a new / delete che si verificano delete
quando la risorsa di loro proprietà esce dall'ambito. Alcuni puntatori intelligenti, come shared_ptr, consentono di dire loro un cosiddetto deleter, che viene utilizzato al posto di delete
. Ciò consente, ad esempio, di gestire handle di finestre, risorse di espressioni regolari e altre cose arbitrarie, purché comunichi a shared_ptr il giusto deleter.
Esistono diversi puntatori intelligenti per scopi diversi:
Codice:
unique_ptr<plot_src> p(new plot_src); // now, p owns
unique_ptr<plot_src> u(move(p)); // now, u owns, p owns nothing.
unique_ptr<plot_src> v(u); // error, trying to copy u
vector<unique_ptr<plot_src>> pv;
pv.emplace_back(new plot_src);
pv.emplace_back(new plot_src);
A differenza di auto_ptr, unique_ptr può essere inserito in un contenitore, poiché i contenitori saranno in grado di contenere tipi non copiabili (ma mobili), come flussi e unique_ptr.
Codice:
void do_something() {
scoped_ptr<pipe> sp(new pipe);
// do something here...
} // when going out of scope, sp will delete the pointer automatically.
Codice:
shared_ptr<plot_src> p(new plot_src(&fx));
plot1->add(p)->setColor("#00FF00");
plot2->add(p)->setColor("#FF0000");
// if p now goes out of scope, the src won't be freed, as both plot1 and
// plot2 both still have references.
Come vedi, la trama-sorgente (funzione fx) è condivisa, ma ognuna ha una voce separata, sulla quale impostare il colore. Esiste una classe weak_ptr che viene utilizzata quando il codice deve fare riferimento alla risorsa di proprietà di un puntatore intelligente, ma non è necessario possedere la risorsa. Invece di passare un puntatore non elaborato, è necessario creare un punto debole. Emetterà un'eccezione quando nota che si tenta di accedere alla risorsa tramite un percorso di accesso weak_ptr, anche se non c'è più shared_ptr che possiede la risorsa.
unique_ptr
e sort
verranno modificati anche allo stesso modo.
RAII è il paradigma di progettazione per garantire che le variabili gestiscano tutte le inizializzazioni necessarie nei loro costruttori e tutte le pulizie necessarie nei loro distruttori. Ciò riduce tutte le inizializzazioni e le pulizie in un solo passaggio.
Il C ++ non richiede RAII, ma è sempre più accettato che l'uso dei metodi RAII produrrà un codice più robusto.
La ragione per cui RAII è utile in C ++ è che C ++ gestisce intrinsecamente la creazione e la distruzione delle variabili quando entrano e escono dall'ambito, sia attraverso il normale flusso di codice sia attraverso lo svolgimento dello stack innescato da un'eccezione. Questo è un omaggio in C ++.
Legando tutta l'inizializzazione e la pulizia a questi meccanismi, sei sicuro che C ++ si occuperà anche di questo lavoro per te.
Parlare di RAII in C ++ di solito porta alla discussione di puntatori intelligenti, perché i puntatori sono particolarmente fragili quando si tratta di pulizia. Quando si gestisce la memoria allocata per heap acquisita da malloc o nuova, di solito è responsabilità del programmatore liberare o eliminare quella memoria prima che il puntatore venga distrutto. I puntatori intelligenti utilizzeranno la filosofia RAII per garantire che gli oggetti allocati in heap vengano distrutti ogni volta che viene distrutta la variabile puntatore.
Il puntatore intelligente è una variante di RAII. RAII significa che l'acquisizione delle risorse è l'inizializzazione. Il puntatore intelligente acquisisce una risorsa (memoria) prima dell'uso e quindi la getta automaticamente in un distruttore. Succedono due cose:
Ad esempio, un altro esempio è il socket di rete RAII. In questo caso:
Ora, come puoi vedere, RAII è uno strumento molto utile nella maggior parte dei casi in quanto aiuta le persone a scopare.
Le fonti C ++ di smart pointer sono in milioni intorno alla rete, comprese le risposte sopra di me.
Boost ha un numero di questi tra cui quelli in Boost.Interprocess per la memoria condivisa. Semplifica notevolmente la gestione della memoria, specialmente in situazioni che inducono mal di testa come quando hai 5 processi che condividono la stessa struttura di dati: quando tutti hanno finito con un pezzo di memoria, vuoi che si liberi automaticamente e non devi sederti lì cercando di capire chi dovrebbe essere responsabile della chiamata delete
a un pezzo di memoria, per non finire con una perdita di memoria o un puntatore che viene erroneamente liberato due volte e potrebbe corrompere l'intero mucchio.
void foo () { std :: stringa bar; // // più codice qui // }
Indipendentemente da ciò che accade, la barra verrà eliminata correttamente una volta lasciato l'ambito della funzione foo ().
Le implementazioni internamente std :: string usano spesso puntatori contati di riferimento. Quindi la stringa interna deve essere copiata solo quando una delle copie delle stringhe è cambiata. Pertanto un puntatore intelligente contato di riferimento consente di copiare qualcosa solo quando necessario.
Inoltre, il conteggio dei riferimenti interni consente di eliminare correttamente la memoria quando non è più necessaria la copia della stringa interna.