c++ - rai finanzas
Cómo manejar la falla del constructor para RAII (6)
Estoy familiarizado con las ventajas de RAII, pero recientemente tropecé con un problema en un código como este:
class Foo
{
public:
Foo()
{
DoSomething();
...
}
~Foo()
{
UndoSomething();
}
}
Todo bien, excepto ese código en la sección del constructor ...
lanzó una excepción, con el resultado de que nunca se llamó a UndoSomething()
.
Hay formas obvias de solucionar ese problema en particular, como ajuste ...
en un bloque try / catch que luego llama a UndoSomething()
, pero a: eso es código duplicado, y los bloques b: try / catch son un olor de código que intento evitar mediante el uso de técnicas RAII. Y, es probable que el código empeore y sea más propenso a errores si hay varios pares de Hacer / Deshacer involucrados, y tenemos que limpiar a la mitad.
Me pregunto si hay un mejor enfoque para hacer esto: tal vez un objeto separado toma un puntero de función e invoca la función cuando, a su vez, se destruye.
class Bar
{
FuncPtr f;
Bar() : f(NULL)
{
}
~Bar()
{
if (f != NULL)
f();
}
}
Sé que no compilará, pero debería mostrar el principio. Foo entonces se convierte ...
class Foo
{
Bar b;
Foo()
{
DoSomething();
b.f = UndoSomething;
...
}
}
Tenga en cuenta que foo ahora no requiere un destructor. ¿Suena más como un problema de lo que vale, o ya es un patrón común con algo útil para aumentar el peso de la carga para mí?
El problema es que tu clase está tratando de hacer demasiado. El principio de RAII es que adquiere un recurso (ya sea en el constructor, o más adelante), y el destructor lo libera; la clase existe únicamente para administrar ese recurso.
En su caso, todo lo que no sea DoSomething()
y UndoSomething()
debe ser responsabilidad del usuario de la clase, no de la clase en sí.
Como dice Steve Jessop en los comentarios: si tiene varios recursos para adquirir, cada uno debe ser administrado por su propio objeto RAII; y podría tener sentido agregarlos como miembros de datos de otra clase que los construya a su vez. Luego, si falla alguna adquisición, todos los recursos previamente adquiridos serán liberados automáticamente por los destructores de los miembros individuales de la clase.
(Además, recuerde la Regla de los Tres ; su clase debe evitar la copia o implementarla de alguna manera sensata para evitar las llamadas múltiples a UndoSomething()
).
Reglas de juego:
- Si su clase está administrando manualmente la creación y eliminación de algo, está haciendo demasiado.
- Si su clase ha escrito manualmente copia-asignación / construcción, es probable que esté administrando demasiado
- Excepción a esto: una clase que tiene el único propósito de administrar exactamente una entidad
Los ejemplos para la tercera regla son std::shared_ptr
, std::unique_ptr
, scope_guard
, std::vector<>
, std::list<>
, scoped_lock
y, por supuesto, la clase Trasher
continuación.
Apéndice.
Podrías ir tan lejos y escribir algo para interactuar con cosas del estilo C:
#include <functional>
#include <iostream>
#include <stdexcept>
class Trasher {
public:
Trasher (std::function<void()> init, std::function<void()> deleter)
: deleter_(deleter)
{
init();
}
~Trasher ()
{
deleter_();
}
// non-copyable
Trasher& operator= (Trasher const&) = delete;
Trasher (Trasher const&) = delete;
private:
std::function<void()> deleter_;
};
class Foo {
public:
Foo ()
: meh_([](){std::cout << "hello!" << std::endl;},
[](){std::cout << "bye!" << std::endl;})
, moo_([](){std::cout << "be or not" << std::endl;},
[](){std::cout << "is the question" << std::endl;})
{
std::cout << "Fooborn." << std::endl;
throw std::runtime_error("oh oh");
}
~Foo() {
std::cout << "Foo in agony." << std::endl;
}
private:
Trasher meh_, moo_;
};
int main () {
try {
Foo foo;
} catch(std::exception &e) {
std::cerr << "error:" << e.what() << std::endl;
}
}
salida:
hello!
be or not
Fooborn.
is the question
bye!
error:oh oh
Entonces, ~Foo()
nunca se ejecuta, pero tu par de inicio / borrado es.
Una cosa buena es: si su función init se lanza, no se invocará su función de eliminación, ya que cualquier excepción lanzada por la función init pasa directamente a través de Trasher()
y, por lo tanto, ~Trasher()
no se ejecutará.
Nota: Es importante que haya un try/catch
más externo, de lo contrario, la norma no requiere el desenrollado de la pila.
Simplemente convierta DoSomething
/ UndoSomething
en un manejador RAII adecuado:
struct SomethingHandle
{
SomethingHandle()
{
DoSomething();
// nothing else. Now the constructor is exception safe
}
SomethingHandle(SomethingHandle const&) = delete; // rule of three
~SomethingHandle()
{
UndoSomething();
}
}
class Foo
{
SomethingHandle something;
public:
Foo() : something() { // all for free
// rest of the code
}
}
Solucionaría esto usando RAII, también:
class Doer
{
Doer()
{ DoSomething(); }
~Doer()
{ UndoSomething(); }
};
class Foo
{
Doer doer;
public:
Foo()
{
...
}
};
El creador se crea antes de que el cuerpo del ctor se inicie y se destruya cuando el destructor falla a través de una excepción o cuando el objeto se destruye normalmente.
Tienes demasiado en tu clase. Mueva DoSomething / UndoSomething a otra clase (''Something''), y tenga un objeto de esa clase como parte de la clase Foo, por lo tanto:
class Foo
{
public:
Foo()
{
...
}
~Foo()
{
}
private:
class Something {
Something() { DoSomething(); }
~Something() { UndoSomething(); }
};
Something s;
}
Ahora, se ha llamado a DoSomething en el momento en que se llama al constructor de Foo, y si el constructor de Foo lanza, entonces se llama correctamente a Deshacer algo.
try / catch no es olor a código en general, debería usarse para manejar errores. En su caso, sin embargo, sería un olor codificado, porque no está manejando un error, simplemente limpiando. Para eso están los destructores.
(1) Si se debe invocar todo en el destructor cuando el constructor falla, simplemente muévalo a una función de limpieza privada, que es llamada por el destructor, y al constructor en caso de falla. Esto parece ser lo que ya has hecho. Buen trabajo.
(2) Una mejor idea es: si hay múltiples pares do / undo que pueden destruir por separado, deberían estar envueltos en su propia pequeña clase RAII, que hace su minitarea, y se limpia solo. No me gusta tu idea actual de darle una función de puntero de limpieza opcional, eso es simplemente confuso. La limpieza siempre debe estar emparejada con la inicialización, ese es el concepto central de RAII.