Cosa si intende per acquisizione di risorse è inizializzazione (RAII)?


Risposte:


374

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


2
@the_mandrill: ho provato ideone.com/1Jjzuc questo programma. Ma non c'è chiamata al distruttore. Il tomdalling.com/blog/software-design/… afferma che C ++ garantisce che verrà chiamato il distruttore di oggetti nello stack, anche se viene generata un'eccezione. Quindi, perché il distruttore non è stato eseguito qui? La mia risorsa è trapelata o non verrà mai liberata o rilasciata?
Distruttore

8
Viene generata un'eccezione, ma non viene rilevata, quindi l'applicazione termina. Se si avvolge con un tentativo {} catch () {} allora funziona come previsto: ideone.com/xm2GR9
the_mandrill

2
Non sono sicuro se Scope-Boundsia 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%
SebNag

125

Questo è un linguaggio di programmazione che significa brevemente che tu

  • incapsulare una risorsa in una classe (il cui costruttore di solito - ma non necessariamente ** - acquisisce la risorsa e il suo distruttore la rilascia sempre)
  • usa la risorsa tramite un'istanza locale della classe *
  • la risorsa viene automaticamente liberata quando l'oggetto esce dall'ambito

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.


1
AFAIK, l'acronimo non implica che l'oggetto debba trovarsi su una variabile locale (stack). Potrebbe essere una variabile membro di un altro oggetto, quindi quando l'oggetto 'holding' viene distrutto, anche l'oggetto membro viene distrutto e la risorsa viene rilasciata. In effetti, penso che l'acronimo significhi specificamente solo che non ci sono 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)
Javier

1
In realtà nulla dice che la risorsa deve essere acquisita nel costruttore. Flussi di file, stringhe e altri contenitori lo fanno, ma la risorsa potrebbe anche essere passata al costruttore, come di solito accade con i puntatori intelligenti. Poiché la tua è la risposta più votata, potresti voler risolvere questo problema.
sabato

Non è un acronimo, è un'abbreviazione. IIRC la maggior parte delle persone lo pronuncia "ar ey ay ay", quindi non si qualifica veramente per un acronimo come dire DARPA, che è pronunciato DARPA invece di farro. Inoltre, direi che RAII è un paradigma piuttosto che un semplice linguaggio.
dtech,

@Peter Torok: ho provato ideone.com/1Jjzuc questo programma. Ma non c'è chiamata al distruttore. Il tomdalling.com/blog/software-design/… afferma che C ++ garantisce che verrà chiamato il distruttore di oggetti nello stack, anche se viene generata un'eccezione. Quindi, perché il distruttore non è stato eseguito qui? La mia risorsa è trapelata o non verrà mai liberata o rilasciata?
Distruttore,

50

"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_ptrpresenta 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.


4
std::auto_ptrè una versione obsoleta di std::unique_ptr. std::auto_ptrtipo di semantica di movimento simulata quanto più possibile in C ++ 98, std::unique_ptrutilizza 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::movetranne che temporanea) mentre era predefinita per qualsiasi copia da non const in std::auto_ptr.
Jan Hudec,

@JiahaoCai: Una volta, molti anni fa (su Usenet), lo stesso Stroustrup lo ha detto.
sabato

21

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 newviene utilizzato per creare un tale oggetto. E per distruggere l'oggetto, l'operatore deletepuò essere utilizzato. Gli oggetti creati dall'operatore newvengono allocati dinamicamente, ovvero allocati nella memoria dinamica (anche chiamata heap o free store ). Quindi, un oggetto che è stato creato newcontinuerà a esistere fino a quando non viene esplicitamente distrutto usando delete.

Alcuni errori che possono verificarsi durante l'utilizzo newe deletesono:

  • Oggetto trapelato (o memoria): usare newper allocare un oggetto e dimenticarlo delete.
  • Eliminazione prematura (o riferimento penzolante ): tenere un altro puntatore su un oggetto, deletel'oggetto, quindi utilizzare l'altro puntatore.
  • Doppia eliminazione : provare deletedue volte su un oggetto.

In generale, sono preferite le variabili con ambito. Tuttavia, RAII può essere utilizzato come alternativa a newe deleteper 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::stringe 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 newper allocare spazio per i suoi elementi nell'heap e deleteper 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_ptre std::lock_guard.

Un altro nome per questa tecnica è SBRM , abbreviazione di Scope-Bound Resource Management .


1
"SBRM" ha molto più senso per me. Sono arrivato a questa domanda perché pensavo di aver capito RAII, ma il nome mi stava buttando via, sentendolo invece descritto come "Scope-Bound Resource Management" mi ha fatto immediatamente capire che avevo davvero capito il concetto.
JShorthouse,

Non sono sicuro del perché questo non sia stato contrassegnato come risposta alla domanda. È una risposta molto approfondita e ben scritta, grazie @elmiomar
Abdelrahman Shoman,

13

Il libro C ++ Programming with Design Patterns Revealed descrive RAII come:

  1. Acquisire tutte le risorse
  2. Utilizzando le risorse
  3. Rilascio di risorse

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.


1
@Brandin Ho modificato il mio post in modo che i lettori si concentrino sul contenuto che conta, piuttosto che discutere l'area grigia della legge sul copyright di ciò che costituisce il fair use.
Dennis,

7

Esistono tre parti per una classe RAII:

  1. La risorsa viene abbandonata nel distruttore
  2. Le istanze della classe sono allocate in pila
  3. La risorsa viene acquisita nel costruttore. Questa parte è facoltativa, ma comune.

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:

  1. Aprire un file
  2. Allocare un po 'di memoria
  3. Acquisire un lucchetto

La parte "è l'inizializzazione" significa che l'acquisizione avviene all'interno del costruttore di una classe.

https://www.tomdalling.com/blog/software-design/resource-acquisition-is-initialisation-raii-explained/


5

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.

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.