C++
RAII: l'acquisition de ressources est une initialisation
Recherche…
Remarques
RAII signifie R eSource A cquisition I s nitialization I. Aussi appelé occasionnellement SBRM (Scope-Based Resource Management) ou RRID (Resource Release Is Destruction), RAII est un langage utilisé pour lier les ressources à la durée de vie de l'objet. En C ++, le destructeur d'un objet s'exécute toujours lorsqu'un objet est hors de portée. Nous pouvons en tirer parti pour lier le nettoyage des ressources à la destruction des objets.
Chaque fois que vous devez acquérir une ressource (par exemple, un verrou, un descripteur de fichier, un tampon alloué) que vous devrez éventuellement libérer, vous devriez envisager d'utiliser un objet pour gérer cette gestion des ressources pour vous. Le déroulement de la pile se produira indépendamment de l'exception ou de la sortie anticipée de la portée. Ainsi, l'objet gestionnaire de ressources nettoiera la ressource pour vous sans que vous ayez à examiner attentivement tous les chemins de code actuels et futurs possibles.
Il convient de noter que RAII ne libère pas complètement le développeur de la durée de vie des ressources. Un cas est évidemment un appel crash ou exit (), qui empêchera les destructeurs d'être appelés. Étant donné que le système d'exploitation va nettoyer les ressources locales telles que la mémoire après la fin d'un processus, ce n'est pas un problème dans la plupart des cas. Cependant, avec les ressources système (c.-à-d. Les canaux nommés, les fichiers de verrouillage, la mémoire partagée), vous avez toujours besoin d'installations pour traiter un processus qui ne s'est pas nettoyé. vérifiez que le processus avec le pid existe réellement, puis agissez en conséquence.
Une autre situation est celle où un processus Unix appelle une fonction de la famille exec, c'est-à-dire après un fork-exec pour créer un nouveau processus. Ici, le processus fils aura une copie complète de la mémoire des parents (y compris les objets RAII), mais une fois qu’il aura été appelé, aucun des destructeurs ne sera appelé dans ce processus. D'un autre côté, si un processus est forké et qu'aucun des processus n'appelle exec, toutes les ressources sont nettoyées dans les deux processus. Ceci est correct uniquement pour toutes les ressources qui ont été réellement dupliquées dans fork, mais avec les ressources système, les deux processus auront uniquement une référence à la ressource (c.-à-d. Le chemin vers un fichier de verrouillage) et tenteront tous deux l'autre processus à échouer.
Verrouillage
Mauvais verrouillage:
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.
}
}
C'est la mauvaise façon d'implémenter le verrouillage et le déverrouillage du mutex. Pour garantir la publication correcte du mutex avec unlock()
le programmeur doit s'assurer que tous les flux entraînant la sortie de la fonction entraînent un appel à unlock()
. Comme indiqué ci-dessus, il s'agit d'un processus fragile car les responsables doivent continuer à suivre le modèle manuellement.
En utilisant une classe spécialement conçue pour implémenter RAII, le problème est trivial:
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
est un modèle de classe extrêmement simple qui appelle simplement lock()
sur son argument dans son constructeur, conserve une référence à l'argument et appelle unlock()
sur l'argument de son destructeur. C'est-à-dire que lorsque le lock_guard
est hors de portée, le mutex
est garanti pour être déverrouillé. Peu importe si la raison pour laquelle elle est hors de portée est une exception ou un retour anticipé - tous les cas sont traités; quel que soit le flux de contrôle, nous avons garanti que nous allons débloquer correctement.
Enfin / ScopeExit
Pour les cas où nous ne voulons pas écrire de classes spéciales pour gérer une ressource, nous pouvons écrire une classe générique:
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)}; }
Et son exemple d'utilisation
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)`
}
Remarque (1): Certaines discussions sur la définition du destructeur doivent être prises en compte pour gérer les exceptions:
-
~Finally() noexcept { f(); }
:std::terminate
est appelé en cas d'exception -
~Finally() noexcept(noexcept(f())) { f(); }
: terminate () est appelé uniquement en cas d'exception lors du déroulement de la pile. -
~Finally() noexcept { try { f(); } catch (...) { /* ignore exception (might log it) */} }
Aucunstd::terminate
n'a été appelé, mais nous ne pouvons pas gérer les erreurs (même pour le déroulement de la pile).
ScopeSuccess (c ++ 17)
Grâce à int std::uncaught_exceptions()
, nous pouvons implémenter une action qui ne s'exécute qu'en cas de succès (aucune exception renvoyée dans la portée). Auparavant bool std::uncaught_exception()
permet juste de détecter si un déroulement de la pile est en cours d' exécution.
#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
}
}
Sortie:
Success 1
ScopeFail (c ++ 17)
Grâce à int std::uncaught_exceptions()
, nous pouvons implémenter une action qui ne s'exécute qu'en cas d'échec (exception levée dans la portée). Auparavant bool std::uncaught_exception()
permet juste de détecter si un déroulement de la pile est en cours d' exécution.
#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
}
}
Sortie:
Failure 2