RAII e puntatori intelligenti in C ++


Risposte:


317

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.


7
Va notato che molte implementazioni di stringhe sono implementate in termini di un puntatore conteggiato di riferimento. Queste semantiche copy-on-write rendono la restituzione di una stringa in base al valore davvero economica.

7
Anche per quelli che non lo sono, molti compilatori implementano l'ottimizzazione NRV che si occuperebbe dell'overhead. In generale, trovo raramente utile share_ptr: basta attenersi a RAII ed evitare la proprietà condivisa.
Nemanja Trifunovic,

27
la restituzione di una stringa non è una buona ragione per usare davvero i puntatori intelligenti. l'ottimizzazione del valore di ritorno può facilmente ottimizzare il rendimento e la semantica di spostamento c ++ 1x eliminerà del tutto una copia (se utilizzata correttamente). Mostra invece un esempio del mondo reale (ad esempio quando condividiamo la stessa risorsa) :)
Johannes Schaub - litb

1
Penso che la tua conclusione all'inizio del motivo per cui Java non può fare questo manca di chiarezza. Il modo più semplice per descrivere questa limitazione in Java o C # è perché non è possibile allocare nello stack. C # consente l'allocazione dello stack tramite una parola chiave speciale, tuttavia si perde la sicurezza del tipo.
ApplePieIsGood,

4
@Nemanja Trifunovic: per RAII in questo contesto intendi restituire copie / creare oggetti in pila? Questo non funziona se hai restituito / accetta oggetti di tipi che possono essere sottoclassati. Quindi devi usare un puntatore per evitare di tagliare l'oggetto, e direi che un puntatore intelligente è spesso migliore di uno grezzo in quei casi.
Frank Osterfeld,

141

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 deletequando 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:

unique_ptr

è un puntatore intelligente che possiede un oggetto esclusivamente. Non è in aumento, ma probabilmente apparirà nel prossimo standard C ++. Non è copiabile ma supporta il trasferimento di proprietà . Alcuni esempi di codice (prossimo C ++):

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.

scoped_ptr

è un puntatore smart boost che non è né copiabile né mobile. È la cosa perfetta da utilizzare quando si desidera assicurarsi che i puntatori vengano eliminati quando si esce dall'ambito.

Codice:

void do_something() {
    scoped_ptr<pipe> sp(new pipe);
    // do something here...
} // when going out of scope, sp will delete the pointer automatically. 

shared_ptr

è per proprietà condivisa. Pertanto, è sia copiabile che mobile. Più istanze di puntatori intelligenti possono possedere la stessa risorsa. Non appena l'ultimo puntatore intelligente proprietario della risorsa non rientra nell'ambito, la risorsa verrà liberata. Alcuni esempi reali di uno dei miei progetti:

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.


Per quanto ne so, gli oggetti non copiabili non sono affatto buoni da usare nei contenitori stl poiché si basano sulla semantica del valore: cosa succede se si desidera ordinare quel contenitore? l'ordinamento copia elementi ...
fmuecke,

I contenitori C ++ 0x verranno cambiati in modo tale da rispettare i tipi di solo spostamento simili unique_ptre sortverranno modificati anche allo stesso modo.
Johannes Schaub - litb

Ricordi dove hai sentito per la prima volta il termine SBRM? James sta cercando di rintracciarlo.
GManNickG,

quali intestazioni o librerie dovrei includere per usarle? ulteriori letture al riguardo?
atoMerz,

Un consiglio qui: se c'è una risposta a una domanda C ++ di @litb, è la risposta giusta (non importa i voti o la risposta contrassegnata come "corretta") ...
fnl

32

La premessa e le ragioni sono semplici, nel concetto.

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.


Inoltre - i puntatori sono l'applicazione più comune di RAII - probabilmente assegnerai migliaia di volte più puntatori rispetto a qualsiasi altra risorsa.
Eclipse,

8

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:

  1. Allociamo la memoria prima di usarla, sempre, anche quando non ne abbiamo voglia: è difficile fare un altro modo con un puntatore intelligente. Se ciò non accadesse, proverai ad accedere alla memoria NULL, causando un arresto anomalo (molto doloroso).
  2. Liberiamo memoria anche quando si verifica un errore. Nessun ricordo è lasciato in sospeso.

Ad esempio, un altro esempio è il socket di rete RAII. In questo caso:

  1. Siamo aperti presa di rete prima che lo usiamo, sempre, anche quando non ci sentiamo come se - è difficile da fare in un altro modo con Raii. Se provi a farlo senza RAII, potresti aprire un socket vuoto, ad esempio connessione MSN. Quindi un messaggio del tipo "facciamolo stasera" potrebbe non essere trasferito, gli utenti non verranno licenziati e potresti rischiare di essere licenziato.
  2. Chiudiamo socket di rete anche quando c'è un errore. Nessun socket viene lasciato in sospeso in quanto ciò potrebbe impedire al messaggio di risposta "sicuramente malato in fondo" di rispondere al mittente.

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.


2

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 deletea 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.


0
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.


1
void f () {Obj x; } Obj x viene eliminato per mezzo della creazione / distruzione del frame dello stack (svolgimento) ... non è correlato al conteggio dei riferimenti.
Hernán,

Il conteggio dei riferimenti è una caratteristica dell'implementazione interna della stringa. RAII è il concetto alla base della cancellazione dell'oggetto quando l'oggetto non rientra nell'ambito. La domanda riguardava RAII e anche i suggerimenti intelligenti.

1
"Indipendentemente da ciò che accade": cosa succede se viene generata un'eccezione prima che la funzione ritorni?
titaniumdecoy,

Quale funzione viene restituita? Se viene generata un'eccezione in foo, la barra viene eliminata. Il costruttore predefinito di lancio di un'eccezione sarebbe un evento straordinario.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.