C++
RAII: Resource Acquisition is initialisatie
Zoeken…
Opmerkingen
RAII staat voor R esource A cquisition I s I nitialization. Ook af en toe SBRM (Scope-Based Resource Management) of RRID (Resource Release Is Destruction) genoemd, is RAII een idioom dat wordt gebruikt om resources te koppelen aan de levensduur van een object. In C ++ wordt de destructor voor een object altijd uitgevoerd wanneer een object buiten bereik valt - we kunnen daar gebruik van maken om resource cleanup te koppelen aan objectvernietiging.
Telkens wanneer u een bepaalde bron (bijvoorbeeld een vergrendeling, een bestandsingang, een toegewezen buffer) moet aanschaffen die u uiteindelijk moet vrijgeven, moet u overwegen een object te gebruiken om dat bronbeheer voor u af te handelen. Het afwikkelen van de stapel gebeurt ongeacht de uitzondering of het vroegtijdig verlaten van het bereik, dus het resource handler-object zal de resource voor u opschonen zonder dat u zorgvuldig alle mogelijke huidige en toekomstige codepaden moet overwegen.
Het is vermeldenswaard dat RAII de ontwikkelaar niet volledig vrijlaat om na te denken over de levensduur van resources. Eén geval is uiteraard een aanroep van crash of exit (), waardoor wordt voorkomen dat destructors worden aangeroepen. Aangezien het besturingssysteem proces-lokale bronnen zoals geheugen opruimt nadat een proces is beëindigd, is dit in de meeste gevallen geen probleem. Met systeembronnen (dat wil zeggen pijpen, vergrendelingsbestanden, gedeeld geheugen) hebt u echter nog steeds voorzieningen nodig om het geval aan te pakken waarbij een proces na zichzelf niet is opgeschoond, dat wil zeggen bij een opstarttest of het vergrendelingsbestand aanwezig is, als dit het geval is, verifieer dat het proces met de pid echt bestaat, handel dan dienovereenkomstig.
Een andere situatie is wanneer een unix-proces een functie uit de exec-familie aanroept, dwz na een fork-exec om een nieuw proces te maken. Hier zal het onderliggende proces een volledige kopie van het geheugen van de ouders hebben (inclusief de RAII-objecten), maar zodra exec werd aangeroepen, zal geen van de destructors in dat proces worden aangeroepen. Aan de andere kant, als een proces gevorkt is en geen van beide processen exec aanroept, worden alle bronnen in beide processen opgeruimd. Dit is alleen correct voor alle bronnen die daadwerkelijk in de vork waren gedupliceerd, maar met systeembronnen hebben beide processen alleen een verwijzing naar de bron (dwz het pad naar een vergrendelingsbestand) en zullen beide proberen deze afzonderlijk vrij te geven, wat mogelijk kan leiden tot het andere proces om te mislukken.
Locking
Slechte vergrendeling:
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.
}
}
Dat is de verkeerde manier om het vergrendelen en ontgrendelen van de mutex te implementeren. Om ervoor te zorgen dat de mutex correct wordt ontgrendeld met unlock()
moet de programer ervoor zorgen dat alle stromen die leiden tot het afsluiten van de functie resulteren in een aanroep voor unlock()
. Zoals hierboven getoond is dit een bros proces omdat het vereist dat onderhouders het patroon handmatig blijven volgen.
Het gebruik van een correct vervaardigde klasse om RAII te implementeren, is het probleem triviaal:
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
is een uiterst eenvoudige klassensjabloon die eenvoudig lock()
oproept in zijn argument in zijn constructor, een verwijzing naar het argument bewaart en unlock()
aanroept in het argument in zijn destructor. Dat wil zeggen dat wanneer de lock_guard
buiten bereik is, de mutex
gegarandeerd wordt ontgrendeld. Het maakt niet uit of de reden dat het buiten bereik is een uitzondering of een vroege terugkeer is - alle zaken worden behandeld; ongeacht de besturingsstroom, hebben we gegarandeerd dat we correct zullen ontgrendelen.
Tenslotte / ScopeExit
Voor gevallen waarin we geen speciale klassen willen schrijven om bepaalde bronnen te verwerken, kunnen we een generieke klasse schrijven:
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)}; }
En zijn voorbeeldgebruik
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)`
}
Opmerking (1): enige discussie over de definitie van destructor moet worden beschouwd als een uitzondering:
-
~Finally() noexcept { f(); }
:std::terminate
wordt opgeroepen in geval van uitzondering -
~Finally() noexcept(noexcept(f())) { f(); }
: terminate () wordt alleen genoemd in uitzonderingsgevallen tijdens het afwikkelen van de stapel. -
~Finally() noexcept { try { f(); } catch (...) { /* ignore exception (might log it) */} }
Geenstd::terminate
aangeroepen, maar we kunnen geen fout verwerken (zelfs voor niet-stack afwikkelen).
ScopeSuccess (c ++ 17)
Dankzij int std::uncaught_exceptions()
kunnen we actie implementeren die alleen bij succes wordt uitgevoerd (geen uitzonderingsbereik). Eerder stond bool std::uncaught_exception()
alleen toe om te detecteren of een bool std::uncaught_exception()
actief is.
#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
}
}
Output:
Success 1
ScopeFail (c ++ 17)
Dankzij int std::uncaught_exceptions()
kunnen we actie implementeren die alleen wordt uitgevoerd bij een mislukking (uitzonderingsfout int std::uncaught_exceptions()
). Eerder stond bool std::uncaught_exception()
alleen toe om te detecteren of een bool std::uncaught_exception()
actief is.
#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
}
}
Output:
Failure 2