C++
RAII: Resursförvärv initieras
Sök…
Anmärkningar
RAII står för R esource A- förvärv I s I initialisering. RAII är också ibland kallad SBRM (Scope-Based Resource Management) eller RRID (Resource Release Is Destruction), ett formspråk som används för att binda resurser till objektets livslängd. I C ++ kör förstöraren för ett objekt alltid när ett objekt går utanför räckvidden - vi kan dra nytta av det för att binda resursrensning till objektförstörelse.
Varje gång du behöver skaffa en resurs (t.ex. ett lås, ett filhandtag, en tilldelad buffert) som du så småningom kommer att behöva släppa, bör du överväga att använda ett objekt för att hantera den resurshantering för dig. Uppspolning av staplar kommer att ske oavsett undantag eller tidig utsträckning, så resurshanteringsobjektet kommer att rensa upp resursen för dig utan att du måste noggrant överväga alla möjliga nuvarande och framtida kodvägar.
Det är värt att notera att RAII inte helt frigör utvecklaren från att tänka på resursernas livslängd. Ett fall är uppenbarligen en krasch- eller utgångssamtal () som förhindrar att förstörare kallas. Eftersom operativsystemet kommer att rensa upp process-lokala resurser som minne efter att en process har avslutats är detta inte ett problem i de flesta fall. Men med systemresurser (dvs. namngivna rör, låsfiler, delat minne) behöver du fortfarande faciliteter för att hantera fallet där en process inte rensat efter sig själv, dvs. vid starttest om låsfilen finns där, om den är, verifiera processen med att pid faktiskt finns och agera därefter.
En annan situation är när en unix-process kallar en funktion från exec-familjen, dvs efter en gaffel-exec för att skapa en ny process. Här kommer barnprocessen att ha en fullständig kopia av föräldrarnas minne (inklusive RAII-objekt) men när exec har kallats kommer ingen av förstörarna att kallas i den processen. Å andra sidan, om en process gaffas och ingen av processerna kallar exec, rensas alla resurser upp i båda processerna. Detta är bara korrekt för alla resurser som faktiskt duplicerats i gaffeln, men med systemresurser kommer båda processerna bara att ha en referens till resursen (dvs. vägen till en låsfil) och kommer båda att försöka släppa den individuellt, vilket kan orsaka den andra processen att misslyckas.
låsning
Dålig låsning:
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.
}
}
Det är fel sätt att implementera låsningen och låsningen av mutex. För att säkerställa rätt frigöring av mutex med unlock()
krävs att programmeraren ser till att alla flöden som resulterar i att funktionen lämnas resulterar i ett samtal om att unlock()
. Som visas ovan är detta en spröd process eftersom det kräver att alla underhållare fortsätter att följa mönstret manuellt.
Att använda en lämpligt utformad klass för att implementera RAII är problemet trivialt:
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
är en extremt enkel klassmall som helt enkelt kallar lock()
för dess argument i sin konstruktör, håller en hänvisning till argumentet och kallar unlock()
för argumentet i sin destruktor. Det vill säga, när lock_guard
går ur omfattning, mutex
garanteras låsas upp. Det spelar ingen roll om skälet till att det gick utanför tillämpningsområdet är ett undantag eller en tidig återkomst - alla ärenden hanteras; oavsett styrflöde har vi garanterat att vi låses upp korrekt.
Slutligen / ScopeExit
För fall där vi inte vill skriva specialklasser för att hantera någon resurs kan vi skriva en generisk klass:
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)}; }
Och dess exempelanvändning
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)`
}
Obs (1): Vissa diskussioner om destruktordefinition måste anses hantera undantag:
-
~Finally() noexcept { f(); }
:std::terminate
kallas vid undantag -
~Finally() noexcept(noexcept(f())) { f(); }
: terminate () kallas endast vid undantag vid stapling av stacken. -
~Finally() noexcept { try { f(); } catch (...) { /* ignore exception (might log it) */} }
Ingastd::terminate
kallas, men vi kan inte hantera fel (även för icke-stack avlindning).
ScopeSuccess (c ++ 17)
Tack vare int std::uncaught_exceptions()
kan vi implementera åtgärder som endast utförs på framgång (inget kastat undantag i omfattning). Tidigare tillåter bool std::uncaught_exception()
bara att upptäcka om någon stack avlindning körs.
#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
}
}
Produktion:
Success 1
ScopeFail (c ++ 17)
Tack vare int std::uncaught_exceptions()
kan vi implementera åtgärder som endast utförs vid fel (kastat undantag i omfattning). Tidigare tillåter bool std::uncaught_exception()
bara att upptäcka om någon stack avlindning körs.
#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
}
}
Produktion:
Failure 2