Cosa si intende per acquisizione di risorse è inizializzazione (RAII)?
Cosa si intende per acquisizione di risorse è inizializzazione (RAII)?
Risposte:
È un nome davvero terribile per un concetto incredibilmente potente, e forse una delle cose numero 1 che gli sviluppatori C ++ mancano quando passano ad altre lingue. C'è stato un po 'di movimento per provare a rinominare questo concetto come Scope-Bound Resource Management , anche se non sembra aver ancora preso piede.
Quando diciamo "Risorsa" non intendiamo solo memoria: potrebbero essere handle di file, socket di rete, handle di database, oggetti GDI ... In breve, cose di cui abbiamo una fornitura limitata e quindi dobbiamo essere in grado di controllare il loro utilizzo. L'aspetto 'Scope-bound' significa che la durata dell'oggetto è legata all'ambito di una variabile, quindi quando la variabile esce dal campo di applicazione il distruttore rilascerà la risorsa. Una proprietà molto utile di ciò è che garantisce una maggiore sicurezza delle eccezioni. Ad esempio, confronta questo:
RawResourceHandle* handle=createNewResource();
handle->performInvalidOperation(); // Oops, throws exception
...
deleteResource(handle); // oh dear, never gets called so the resource leaks
Con quello RAII
class ManagedResourceHandle {
public:
ManagedResourceHandle(RawResourceHandle* rawHandle_) : rawHandle(rawHandle_) {};
~ManagedResourceHandle() {delete rawHandle; }
... // omitted operator*, etc
private:
RawResourceHandle* rawHandle;
};
ManagedResourceHandle handle(createNewResource());
handle->performInvalidOperation();
In quest'ultimo caso, quando viene generata l'eccezione e lo stack viene srotolato, le variabili locali vengono distrutte, il che assicura che la nostra risorsa venga ripulita e non presenti perdite.
Scope-Bound
sia la scelta del nome migliore qui poiché gli identificatori della classe di archiviazione insieme all'ambito determinano la durata della memoria di un'entità. Restringere il campo di applicazione è forse una semplificazione utile, tuttavia non è preciso al 100%
Questo è un linguaggio di programmazione che significa brevemente che tu
Ciò garantisce che qualunque cosa accada mentre la risorsa è in uso, alla fine verrà liberata (a causa del normale ritorno, della distruzione dell'oggetto contenitore o di un'eccezione generata).
È una buona pratica ampiamente usata in C ++, perché oltre ad essere un modo sicuro per gestire le risorse, rende anche il tuo codice molto più pulito in quanto non è necessario mescolare il codice di gestione degli errori con la funzionalità principale.
*
Aggiornamento: "locale" può significare una variabile locale o una variabile membro non statica di una classe. In quest'ultimo caso la variabile membro viene inizializzata e distrutta con l'oggetto proprietario.
**
Update2: come sottolineato da @sbi, la risorsa - sebbene spesso sia allocata all'interno del costruttore - può anche essere allocata all'esterno e passata come parametro.
open()
/ close()
metodi per inizializzare e rilasciare la risorsa, solo il costruttore e il distruttore, quindi il "mantenimento" della risorsa è solo la durata dell'oggetto, non importa se quella vita è gestito dal contesto (stack) o esplicitamente (allocazione dinamica)
"RAII" sta per "Acquisizione delle risorse è inizializzazione" ed è in realtà un termine improprio, dal momento che non si tratta di acquisizione di risorse (e inizializzazione di un oggetto) di cui si occupa, ma di rilasciare la risorsa (mediante la distruzione di un oggetto ).
Ma RAII è il nome che abbiamo e si attacca.
Al suo centro, l'idioma presenta risorse incapsulanti (blocchi di memoria, file aperti, mutex sbloccati, tu-lo-nominalo) in oggetti locali, automatici e il distruttore di quell'oggetto che rilascia la risorsa quando l'oggetto viene distrutto fine dell'ambito di appartenenza:
{
raii obj(acquire_resource());
// ...
} // obj's dtor will call release_resource()
Naturalmente, gli oggetti non sono sempre oggetti automatici locali. Potrebbero anche essere membri di una classe:
class something {
private:
raii obj_; // will live and die with instances of the class
// ...
};
Se tali oggetti gestiscono la memoria, vengono spesso chiamati "puntatori intelligenti".
Ci sono molte varianti di questo. Ad esempio, nei primi frammenti di codice sorge la domanda che cosa accadrebbe se qualcuno volesse copiare obj
. La via più semplice sarebbe quella di non consentire la copia. std::unique_ptr<>
, un puntatore intelligente per far parte della libreria standard come descritto dal prossimo standard C ++, fa questo.
Un altro puntatore così intelligente, std::shared_ptr
presenta la "proprietà condivisa" della risorsa (un oggetto allocato dinamicamente) che contiene. Cioè, può essere liberamente copiato e tutte le copie si riferiscono allo stesso oggetto. Il puntatore intelligente tiene traccia di quante copie si riferiscono allo stesso oggetto e lo eliminerà quando l'ultimo viene distrutto.
Una terza variante è descritta dastd::auto_ptr
che implementa una sorta di semantica di movimento: un oggetto è di proprietà di un solo puntatore e il tentativo di copiare un oggetto comporterà (attraverso l'hacking di sintassi) il trasferimento della proprietà dell'oggetto sulla destinazione dell'operazione di copia.
std::auto_ptr
è una versione obsoleta di std::unique_ptr
. std::auto_ptr
tipo di semantica di movimento simulata quanto più possibile in C ++ 98, std::unique_ptr
utilizza la nuova semantica di movimento di C ++ 11. La nuova classe è stata creata perché la semantica di spostamento di C ++ 11 è più esplicita (richiede std::move
tranne che temporanea) mentre era predefinita per qualsiasi copia da non const in std::auto_ptr
.
La durata di un oggetto è determinata dal suo ambito. Tuttavia, a volte abbiamo bisogno, o è utile, di creare un oggetto che vive indipendentemente dall'ambito in cui è stato creato. In C ++, l'operatore new
viene utilizzato per creare un tale oggetto. E per distruggere l'oggetto, l'operatore delete
può essere utilizzato. Gli oggetti creati dall'operatore new
vengono allocati dinamicamente, ovvero allocati nella memoria dinamica (anche chiamata heap o free store ). Quindi, un oggetto che è stato creato new
continuerà a esistere fino a quando non viene esplicitamente distrutto usando delete
.
Alcuni errori che possono verificarsi durante l'utilizzo new
e delete
sono:
new
per allocare un oggetto e dimenticarlo delete
.delete
l'oggetto, quindi utilizzare l'altro puntatore.delete
due volte su un oggetto.In generale, sono preferite le variabili con ambito. Tuttavia, RAII può essere utilizzato come alternativa a new
e delete
per far vivere un oggetto indipendentemente dal suo ambito. Tale tecnica consiste nel prendere il puntatore sull'oggetto allocato sull'heap e posizionarlo in un oggetto handle / manager . Quest'ultimo ha un distruttore che si occuperà di distruggere l'oggetto. Ciò garantirà che l'oggetto sia disponibile per qualsiasi funzione che desideri accedervi e che l'oggetto venga distrutto al termine della durata dell'oggetto handle , senza la necessità di una pulizia esplicita.
Esempi dalla libreria standard C ++ che utilizzano RAII sono std::string
e std::vector
.
Considera questo pezzo di codice:
void fn(const std::string& str)
{
std::vector<char> vec;
for (auto c : str)
vec.push_back(c);
// do something
}
quando crei un vettore e spingi elementi su di esso, non ti importa di allocare e deallocare tali elementi. Il vettore utilizza new
per allocare spazio per i suoi elementi nell'heap e delete
per liberare quello spazio. Tu come utente del vettore non ti preoccupi dei dettagli di implementazione e ti fiderai che il vettore non perda. In questo caso, il vettore è l' oggetto handle dei suoi elementi.
Altri esempi della libreria standard che uso Raii sono std::shared_ptr
, std::unique_ptr
e std::lock_guard
.
Un altro nome per questa tecnica è SBRM , abbreviazione di Scope-Bound Resource Management .
Il libro C ++ Programming with Design Patterns Revealed descrive RAII come:
Dove
Le risorse sono implementate come classi e tutti i puntatori hanno involucri di classe attorno a loro (rendendoli puntatori intelligenti).
Le risorse vengono acquisite invocando i loro costruttori e rilasciate implicitamente (in ordine inverso rispetto all'acquisizione) invocando i loro distruttori.
Esistono tre parti per una classe RAII:
RAII sta per "L'acquisizione delle risorse è l'inizializzazione". La parte "acquisizione di risorse" di RAII è dove inizi qualcosa che deve essere terminato in seguito, come:
La parte "è l'inizializzazione" significa che l'acquisizione avviene all'interno del costruttore di una classe.
La gestione manuale della memoria è un incubo che i programmatori hanno inventato modi per evitare dall'invenzione del compilatore. I linguaggi di programmazione con i garbage collector semplificano la vita, ma a costo di prestazioni. In questo articolo - Eliminare il Garbage Collector: The RAII Way , l'ingegnere di Toptal Peter Goodspeed-Niklaus ci dà una sbirciatina nella storia dei netturbini e spiega come le nozioni di proprietà e prestito possono aiutare ad eliminare i netturbini senza compromettere le loro garanzie di sicurezza.