C++
RAII: l'acquisizione delle risorse è inizializzata
Ricerca…
Osservazioni
RAII è l'acronimo di R esource A cquisition I s I nitialization. Anche a volte indicato come SBRM (Scope-Based Resource Management) o RRID (Resource Release Is Destruction), RAII è un idioma utilizzato per legare le risorse alla durata dell'oggetto. In C ++, il distruttore per un oggetto viene sempre eseguito quando un oggetto esce dall'ambito di applicazione - possiamo trarne vantaggio per collegare la pulizia delle risorse alla distruzione dell'oggetto.
Ogni volta che è necessario acquisire alcune risorse (ad es. Un blocco, un handle di file, un buffer allocato) che sarà necessario rilasciare, si dovrebbe prendere in considerazione l'utilizzo di un oggetto per gestire tale gestione delle risorse. Lo sbobinamento dello stack avverrà indipendentemente dall'eccezione o dall'uscita dell'ambito iniziale, quindi l'oggetto gestore risorse ripulirà la risorsa per te senza dover considerare attentamente tutti i possibili percorsi di codice corrente e futuro.
Vale la pena notare che RAII non completamente libera lo sviluppatore di pensare alla durata delle risorse. Un caso è, ovviamente, una chiamata crash o exit (), che impedirà il richiamo dei distruttori. Dal momento che il SO pulirà il processo, le risorse locali come la memoria dopo la fine di un processo, questo non è un problema nella maggior parte dei casi. Tuttavia con le risorse di sistema (ad es. Named pipe, lock file, shared memory) hai ancora bisogno di strutture per gestire il caso in cui un processo non si è ripulito da solo, cioè all'avvio test se il file di lock è lì, se lo è, verificare il processo con il pid effettivamente esiste, quindi agire di conseguenza.
Un'altra situazione è quando un processo unix chiama una funzione della famiglia exec, cioè dopo un fork-exec per creare un nuovo processo. Qui, il processo figlio avrà una copia completa della memoria dei genitori (inclusi gli oggetti RAII) ma una volta che è stato chiamato exec, nessuno dei distruttori sarà chiamato in quel processo. D'altra parte, se un processo è biforcato e nessuno dei processi chiama exec, tutte le risorse vengono ripulite in entrambi i processi. Questo è corretto solo per tutte le risorse effettivamente duplicate nel fork, ma con le risorse di sistema, entrambi i processi avranno solo un riferimento alla risorsa (cioè il percorso di un file di blocco) e cercheranno entrambi di rilasciarlo individualmente, causando potenzialmente l'altro processo fallisce.
Blocco
Cattivo bloccaggio:
std::mutex mtx;
void bad_lock_example() {
mtx.lock();
try
{
foo();
bar();
if (baz()) {
mtx.unlock(); // Have to unlock on each exit point.
return;
}
quux();
mtx.unlock(); // Normal unlock happens here.
}
catch(...) {
mtx.unlock(); // Must also force unlock in the presence of
throw; // exceptions and allow the exception to continue.
}
}
Questo è il modo sbagliato per implementare il blocco e lo sblocco del mutex. Per garantire il corretto rilascio del mutex con unlock()
necessario che il programmatore si assicuri che tutti i flussi risultanti dall'uscita della funzione risultino in una chiamata a unlock()
. Come mostrato sopra, si tratta di processi fragili in quanto richiede a qualsiasi manutentore di continuare a seguire il modello manualmente.
Usando una classe appropriatamente predisposta per implementare RAII, il problema è banale:
std::mutex mtx;
void good_lock_example() {
std::lock_guard<std::mutex> lk(mtx); // constructor locks.
// destructor unlocks. destructor call
// guaranteed by language.
foo();
bar();
if (baz()) {
return;
}
quux();
}
lock_guard
è un modello di classe estremamente semplice che chiama semplicemente lock()
sul suo argomento nel suo costruttore, mantiene un riferimento all'argomento e chiama unlock()
sull'argomento nel suo distruttore. Cioè, quando lock_guard
esce dallo scope, il mutex
è garantito per essere sbloccato. Non importa se il motivo per cui è andato fuori ambito è un'eccezione o un ritorno anticipato - tutti i casi sono gestiti; indipendentemente dal flusso di controllo, abbiamo garantito che sbloccheremo correttamente.
Infine / ScopeExit
Per i casi in cui non vogliamo scrivere classi speciali per gestire alcune risorse, possiamo scrivere una classe generica:
template<typename Function>
class Finally final
{
public:
explicit Finally(Function f) : f(std::move(f)) {}
~Finally() { f(); } // (1) See below
Finally(const Finally&) = delete;
Finally(Finally&&) = default;
Finally& operator =(const Finally&) = delete;
Finally& operator =(Finally&&) = delete;
private:
Function f;
};
// Execute the function f when the returned object goes out of scope.
template<typename Function>
auto onExit(Function &&f) { return Finally<std::decay_t<Function>>{std::forward<Function>(f)}; }
E il suo utilizzo di esempio
void foo(std::vector<int>& v, int i)
{
// ...
v[i] += 42;
auto autoRollBackChange = onExit([&](){ v[i] -= 42; });
// ... code as recursive call `foo(v, i + 1)`
}
Nota (1): alcune discussioni sulla definizione del distruttore devono essere considerate per gestire l'eccezione:
-
~Finally() noexcept { f(); }
:std::terminate
viene chiamato in caso di eccezione -
~Finally() noexcept(noexcept(f())) { f(); }
: terminate () viene chiamato solo in caso di eccezione durante lo srotolamento dello stack. -
~Finally() noexcept { try { f(); } catch (...) { /* ignore exception (might log it) */} }
Nostd::terminate
chiamato, ma non possiamo gestire l'errore (anche per lo stacco non stack).
ScopeSuccess (c ++ 17)
Grazie a int std::uncaught_exceptions()
, possiamo implementare azioni che vengono eseguite solo in caso di successo (nessuna eccezione generata nell'ambito). In precedenza bool std::uncaught_exception()
consente solo di rilevare se è in esecuzione uno srotolamento dello stack.
#include <exception>
#include <iostream>
template <typename F>
class ScopeSuccess
{
private:
F f;
int uncaughtExceptionCount = std::uncaught_exceptions();
public:
explicit ScopeSuccess(const F& f) : f(f) {}
ScopeSuccess(const ScopeSuccess&) = delete;
ScopeSuccess& operator =(const ScopeSuccess&) = delete;
// f() might throw, as it can be caught normally.
~ScopeSuccess() noexcept(noexcept(f())) {
if (uncaughtExceptionCount == std::uncaught_exceptions()) {
f();
}
}
};
struct Foo {
~Foo() {
try {
ScopeSuccess logSuccess{[](){std::cout << "Success 1\n";}};
// Scope succeeds,
// even if Foo is destroyed during stack unwinding
// (so when 0 < std::uncaught_exceptions())
// (or previously std::uncaught_exception() == true)
} catch (...) {
}
try {
ScopeSuccess logSuccess{[](){std::cout << "Success 2\n";}};
throw std::runtime_error("Failed"); // returned value
// of std::uncaught_exceptions increases
} catch (...) { // returned value of std::uncaught_exceptions decreases
}
}
};
int main()
{
try {
Foo foo;
throw std::runtime_error("Failed"); // std::uncaught_exceptions() == 1
} catch (...) { // std::uncaught_exceptions() == 0
}
}
Produzione:
Success 1
ScopeFail (c ++ 17)
Grazie a int std::uncaught_exceptions()
, possiamo implementare un'azione che viene eseguita solo in caso di errore (eccezione generata nell'ambito). In precedenza bool std::uncaught_exception()
consente solo di rilevare se è in esecuzione uno srotolamento dello stack.
#include <exception>
#include <iostream>
template <typename F>
class ScopeFail
{
private:
F f;
int uncaughtExceptionCount = std::uncaught_exceptions();
public:
explicit ScopeFail(const F& f) : f(f) {}
ScopeFail(const ScopeFail&) = delete;
ScopeFail& operator =(const ScopeFail&) = delete;
// f() should not throw, else std::terminate is called.
~ScopeFail() {
if (uncaughtExceptionCount != std::uncaught_exceptions()) {
f();
}
}
};
struct Foo {
~Foo() {
try {
ScopeFail logFailure{[](){std::cout << "Fail 1\n";}};
// Scope succeeds,
// even if Foo is destroyed during stack unwinding
// (so when 0 < std::uncaught_exceptions())
// (or previously std::uncaught_exception() == true)
} catch (...) {
}
try {
ScopeFail logFailure{[](){std::cout << "Failure 2\n";}};
throw std::runtime_error("Failed"); // returned value
// of std::uncaught_exceptions increases
} catch (...) { // returned value of std::uncaught_exceptions decreases
}
}
};
int main()
{
try {
Foo foo;
throw std::runtime_error("Failed"); // std::uncaught_exceptions() == 1
} catch (...) { // std::uncaught_exceptions() == 0
}
}
Produzione:
Failure 2