C++
RAII: la adquisición de recursos es la inicialización
Buscar..
Observaciones
RAII significa R isource A cquisition I s I nitialization. RAII es una expresión idiomática utilizada para vincular los recursos con la vida útil de los objetos. También se conoce como SBRM (Administración de recursos basada en el alcance) o RRID (La publicación del recurso es destrucción). En C ++, el destructor para un objeto siempre se ejecuta cuando un objeto se sale del alcance, podemos aprovechar eso para vincular la limpieza de recursos con la destrucción de objetos.
Cada vez que necesite adquirir algún recurso (por ejemplo, un bloqueo, un identificador de archivo, un búfer asignado) que eventualmente necesitará liberar, debe considerar usar un objeto para manejar esa administración de recursos por usted. El desenrollado de la pila ocurrirá independientemente de la excepción o la salida temprana del alcance, por lo que el objeto del controlador de recursos limpiará el recurso por usted sin que tenga que considerar cuidadosamente todas las rutas de código actuales y futuras posibles.
Vale la pena señalar que RAII no libera completamente al desarrollador de pensar en la vida útil de los recursos. Un caso es, obviamente, una llamada crash o exit (), que evitará que se llame a los destructores. Como el sistema operativo limpiará los recursos locales del proceso, como la memoria, después de que finalice el proceso, esto no es un problema en la mayoría de los casos. Sin embargo, con los recursos del sistema (es decir, canalizaciones con nombre, archivos de bloqueo, memoria compartida), todavía se necesitan recursos para tratar el caso en el que un proceso no se limpió después de sí mismo, es decir, en la prueba de inicio si el archivo de bloqueo está ahí, si es así, Verifique que el proceso con el pid realmente existe, luego actúe en consecuencia.
Otra situación es cuando un proceso de Unix llama a una función de la familia exec, es decir, después de un fork-exec para crear un nuevo proceso. Aquí, el proceso hijo tendrá una copia completa de la memoria de los padres (incluidos los objetos RAII), pero una vez que se llamó a exec, no se llamará a ninguno de los destructores en ese proceso. Por otro lado, si un proceso se bifurca y ninguno de los procesos llama exec, todos los recursos se limpian en ambos procesos. Esto es correcto solo para todos los recursos que realmente se duplicaron en la bifurcación, pero con los recursos del sistema, ambos procesos solo tendrán una referencia al recurso (es decir, la ruta de acceso a un archivo de bloqueo) y ambos intentarán liberarlo individualmente, lo que podría causar El otro proceso para fallar.
Cierre
Bloqueo incorrecto:
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.
}
}
Esa es la forma incorrecta de implementar el bloqueo y desbloqueo del mutex. Para garantizar la correcta liberación del mutex con unlock()
, el programador debe asegurarse de que todos los flujos que resultan en la salida de la función resulten en una llamada a unlock()
. Como se muestra arriba, este es un proceso frágil ya que requiere que los mantenedores continúen siguiendo el patrón manualmente.
El uso de una clase diseñada apropiadamente para implementar RAII, el problema es 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
es una plantilla de clase extremadamente simple que simplemente llama a lock()
en su argumento en su constructor, mantiene una referencia al argumento y llama a unlock()
en el argumento en su destructor. Es decir, cuando el lock_guard
queda fuera del alcance, se garantiza que el mutex
está desbloqueado. No importa si la razón por la que se salió del alcance es una excepción o una devolución anticipada: todos los casos se manejan; independientemente del flujo de control, garantizamos que desbloquearemos correctamente.
Finalmente / ScopeExit
Para los casos en que no queremos escribir clases especiales para manejar algún recurso, podemos escribir una clase genérica:
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)}; }
Y su uso de ejemplo.
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): Debe considerarse alguna discusión sobre la definición del destructor para manejar la excepción:
-
~Finally() noexcept { f(); }
:std::terminate
se llama en caso de excepción -
~Finally() noexcept(noexcept(f())) { f(); }
: terminate () se llama solo en caso de excepción durante el desenrollado de la pila. -
~Finally() noexcept { try { f(); } catch (...) { /* ignore exception (might log it) */} }
No se llama astd::terminate
, pero no podemos manejar el error (incluso para el no desenrollado de la pila).
ScopeSuccess (c ++ 17)
Gracias a int std::uncaught_exceptions()
, podemos implementar acciones que se ejecutan solo en caso de éxito (no se produce ninguna excepción en el alcance). Anteriormente, bool std::uncaught_exception()
solo permite detectar si se está ejecutando algún desenrollado de pila.
#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
}
}
Salida:
Success 1
ScopeFail (c ++ 17)
Gracias a int std::uncaught_exceptions()
, podemos implementar una acción que se ejecuta solo en caso de fallo (se produce una excepción en el alcance). Anteriormente, bool std::uncaught_exception()
solo permite detectar si se está ejecutando algún desenrollado de pila.
#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
}
}
Salida:
Failure 2