c++ - RAII vs. excepciones
exception destructor (7)
¿Cuáles son las razones por las cuales tu destrucción puede fallar? ¿Por qué no mirar a manejar esos antes de realmente destruir?
Por ejemplo, cerrar una conexión de base de datos puede deberse a que:
- Transacción en progreso. (Check std :: uncaught_exception () - si es verdadero, rollback, else commit - estas son las acciones deseadas más probables a menos que tenga una política que diga lo contrario, antes de cerrar la conexión).
- La conexión se corta. (Detectar e ignorar. El servidor se revertirá automáticamente).
- Otro error de DB. (Regístrese para que podamos investigar y posiblemente manejar adecuadamente en el futuro. Que puede ser para detectar e ignorar. Mientras tanto, intente retroceder y desconectar nuevamente e ignorar todos los errores).
Si entiendo RAII correctamente (lo que podría no serlo), el punto es su alcance. Por lo tanto, no desea que las transacciones duren más que el objeto de todos modos. Me parece razonable, entonces, que quiera asegurarse el cierre lo mejor que pueda. RAII no lo hace único: incluso sin objetos (por ejemplo, en C), aún trataría de detectar todas las condiciones de error y tratarlas lo mejor que pueda (lo que a veces es ignorarlas). Todo lo que hace RAII es forzarlo a poner todo ese código en un solo lugar, sin importar cuántas funciones use ese tipo de recurso.
Cuanto más utilizamos RAII en C ++, más nos encontramos con destructores que realizan una desasignación no trivial. Ahora, la desasignación (finalización, como quiera llamarla) puede fallar, en cuyo caso las excepciones son realmente la única forma de que alguien en el piso superior conozca nuestro problema de desasignación. Pero, una vez más, los throw-destructores son una mala idea debido a la posibilidad de que se emitan excepciones durante el desenrollado de la pila. std::uncaught_exception()
permite saber cuándo sucede eso, pero no mucho más, por lo tanto, aparte de dejarle registrar un mensaje antes de la terminación, no hay mucho que pueda hacer, a menos que esté dispuesto a dejar su programa en un estado indefinido, donde algunas cosas están desasignadas / finalizadas y otras no.
Un enfoque es tener destructores sin tiro. Pero en muchos casos eso oculta un error real. Nuestro destructor podría, por ejemplo, cerrar algunas conexiones DB gestionadas por RAII como resultado de una excepción lanzada, y esas conexiones DB podrían no cerrarse. Esto no necesariamente significa que estamos de acuerdo con que el programa termine en este punto. Por otro lado, registrar y rastrear estos errores no es realmente una solución para cada caso; de lo contrario, no habríamos necesitado excepciones para comenzar. Con los destructores de no-tiro también nos encontramos teniendo que crear funciones de "reinicio ()" que se supone que se deben invocar antes de la destrucción, pero eso acaba con todo el propósito de RAII.
Otro enfoque es simplemente dejar que el programa finalice , ya que es lo más predecible que puede hacer.
Algunas personas sugieren encadenar excepciones, por lo que se puede manejar más de un error a la vez. Pero, sinceramente, nunca lo he visto hecho en C ++ y no tengo idea de cómo implementar tal cosa.
Entonces es RAII o excepciones. ¿No es así? Me estoy inclinando hacia los destructores de tiro limpio; principalmente porque mantiene las cosas simples (r). Pero realmente espero que haya una solución mejor, porque, como dije, cuanto más usamos RAII, más nos encontramos usando dtors que hacen cosas no triviales.
Apéndice
Estoy agregando enlaces a interesantes artículos en el tema y discusiones que he encontrado:
- Tirando Destructores
- Discusión de StackOverflow sobre los problemas con SEH
- Discusión de StackOverflow sobre throwing-destructors (gracias, Martin York)
- Joel en Excepciones
- SEH Considerado Dañino
- Manejo de excepciones CLR que también toca el encadenamiento de excepciones
- Herb Sutter en std :: uncaught_exception y por qué no es tan útil como crees
- Discusión histórica sobre el tema con participantes interesantes (¡mucho tiempo!)
- Stroustrup explicando RAII
- Guardia de alcance de Andrei Alexandrescu
De la pregunta original:
Ahora, la desasignación (finalización, como quiera llamarla) puede fallar, en cuyo caso las excepciones son realmente la única forma de que alguien en el piso superior conozca nuestro problema de desasignación.
La falla en la limpieza de un recurso indica:
Error del programador, en cuyo caso, debe registrar el error, seguido de notificación al usuario o finalización de la aplicación, según el escenario de la aplicación. Por ejemplo, liberando una asignación que ya ha sido liberada.
Error del localizador o defecto de diseño. Consulte la documentación. Es probable que el error esté allí para ayudar a diagnosticar errores del programador. Ver ítem 1 arriba.
De lo contrario, condición adversa irrecuperable que puede continuar.
Por ejemplo, la tienda gratuita de C ++ tiene una eliminación de operador sin fallas. Otras API (como Win32) proporcionan códigos de error, pero solo fallarán debido a error del programador o falla de hardware, con errores que indican condiciones como corrupción de montón, doble libre, etc.
En cuanto a las condiciones adversas irrecuperables, tome la conexión DB. Si el cierre de la conexión falló debido a que la conexión se cayó, genial, listo. No tirar! Una conexión desconectada (debería) provocará una conexión cerrada, por lo que no es necesario hacer nada más. En todo caso, registre un mensaje de seguimiento para ayudar a diagnosticar problemas de uso. Ejemplo:
class DBCon{
public:
DBCon() {
handle = fooOpenDBConnection();
}
~DBCon() {
int err = fooCloseDBConnection();
if(err){
if(err == E_fooConnectionDropped){
// do nothing. must have timed out
} else if(fooIsCriticalError(err)){
// critical errors aren''t recoverable. log, save
// restart information, and die
std::clog << "critical DB error: " << err << "/n";
save_recovery_information();
std::terminate();
} else {
// log, in case we need to gather this info in the future,
// but continue normally.
std::clog << "non-critical DB error: " << err << "/n";
}
}
// done!
}
};
Ninguna de estas condiciones justifica intentar un segundo tipo de desenvolvimiento. O el programa puede continuar normalmente (incluido el desenrollado de la excepción, si el desenrollado está en curso), o se muere aquí y ahora.
Editar-Agregar
Si realmente desea mantener algún tipo de vínculo a esas conexiones de bases de datos que no se pueden cerrar (quizás no se cerraron debido a condiciones intermitentes y desea volver a intentarlo más tarde), siempre puede diferir la limpieza. :
vector<DBHandle> to_be_closed_later; // startup reserves space
DBCon::~DBCon(){
int err = fooCloseDBConnection();
if(err){
..
else if( fooIsRetryableError(err) ){
try{
to_be_closed.push_back(handle);
} catch (const bad_alloc&){
std::clog << "could not close connection, err " << err << "/n"
}
}
}
}
No es muy bonito, pero podría hacer el trabajo por ti.
Estás viendo dos cosas:
- RAII, que garantiza que los recursos se limpien cuando se sale del alcance.
- Completar una operación y averiguar si tuvo éxito o no.
RAII promete que completará la operación (memoria libre, cierre el archivo intentando tirarlo, termine una transacción que haya intentado cometerlo). Pero debido a que ocurre automáticamente, sin que el programador tenga que hacer nada, no le dice al programador si las operaciones que "intentó" tuvieron éxito o no.
Las excepciones son una forma de informar que algo falló, pero como dices, hay una limitación del lenguaje C ++ que significa que no son adecuadas para hacer eso desde un destructor [*]. Los valores de retorno son de otra manera, pero es incluso más obvio que los destructores tampoco pueden usarlos.
Entonces, si quiere saber si sus datos fueron escritos en un disco, no puede usar RAII para eso. No "derrota todo el propósito de RAII", ya que RAII aún intentará escribirlo, y aún liberará los recursos asociados con el manejador de archivo (transacción DB, lo que sea). Limita lo que RAII puede hacer: no le informará si los datos fueron escritos o no, por lo que para eso necesita una función close()
que pueda devolver un valor y / o arrojar una excepción.
[*] También es una limitación bastante natural, presente en otros idiomas. Si crees que los destructores RAII deberían arrojar excepciones para decir "¡algo salió mal!", Entonces algo tiene que suceder cuando ya hay una excepción en el vuelo, es decir, "¡algo más ha ido mal incluso antes de eso!". Los idiomas que conozco que usan excepciones no permiten dos excepciones en el vuelo a la vez: el lenguaje y la sintaxis simplemente no lo permiten. Si RAII debe hacer lo que usted desea, las excepciones deben redefinirse para que tenga sentido que un hilo tenga más de una cosa a la vez, y para que se propaguen dos excepciones hacia afuera y dos manejadores, uno para manejar cada uno
Otros lenguajes permiten que la segunda excepción oscurezca la primera, por ejemplo, si un bloque finally
arroja Java. C ++ prácticamente dice que el segundo debe ser suprimido, de lo contrario se llama terminate
(suprimiendo ambos, en cierto sentido). En ninguno de los casos, los niveles de pila superiores están informados de ambas fallas. Lo que es un poco desafortunado es que en C ++ no se puede decir con certeza si una excepción más es demasiada (la excepción no uncaught_exception
no te dice eso, te dice algo diferente), por lo que ni siquiera puedes arrojar el caso donde no hay ya una excepción en vuelo. Pero incluso si pudieras hacerlo en ese caso, todavía estarías lleno en el caso en que uno más es uno demasiado.
Me recuerda una pregunta de un colega cuando le expliqué los conceptos de excepción / RAII: "Oye, ¿qué excepción puedo lanzar si la computadora está apagada?"
De todos modos, estoy de acuerdo con la respuesta de Martin York RAII vs. excepciones
¿Cuál es el trato con Exceptions and Destructors?
Muchas de las características de C ++ dependen de destructores que no lanzan.
De hecho, todo el concepto de RAII y su cooperación con la bifurcación de códigos (devoluciones, lanzamientos, etc.) se basa en el hecho de que la desasignación no fallará. De la misma manera, se supone que algunas funciones no deben fallar (como std :: swap) cuando desea ofrecer garantías de excepción altas a sus objetos.
No es que no signifique que no puedes arrojar excepciones a través de destructores. Solo que el lenguaje ni siquiera intentará apoyar este comportamiento.
¿Qué pasaría si estuviera autorizado?
Solo por diversión, traté de imaginarlo ...
En caso de que tu destructor no libere tu recurso, ¿qué harás? Tu objeto probablemente está medio destruido, ¿qué harías con una captura "externa" con esa información? ¿Inténtalo de nuevo? (En caso afirmativo, ¿por qué no intentarlo nuevamente desde el destructor? ...)
Es decir, si pudieras acceder a tu objeto medio destruido de todos modos: ¿y si tu objeto está en la pila (que es la forma básica en que RAII funciona)? ¿Cómo se puede acceder a un objeto fuera de su alcance?
¿Enviando el recurso dentro de la excepción?
Su única esperanza sería enviar el "identificador" del recurso dentro de la excepción y el código de espera en la captura, bueno ... ¿volver a tratar de desasignarlo (ver arriba)?
Ahora, imagina algo gracioso:
void doSomething()
{
try
{
MyResource A, B, C, D, E ;
// do something with A, B, C, D and E
// Now we quit the scope...
// destruction of E, then D, then C, then B and then A
}
catch(const MyResourceException & e)
{
// Do something with the exception...
}
}
Ahora, imaginemos por alguna razón que el destructor de D no puede desasignar el recurso. Usted lo codificó para enviar una excepción, que será atrapada por la captura. Todo va bien: puedes manejar la falla de la forma que quieras (cómo aún lo harás de una manera constructiva aún se me escapa, pero entonces, no es el problema ahora).
Pero...
¿Enviar los recursos MÚLTIPLES dentro de las excepciones MÚLTIPLES?
Ahora, si ~ D puede fallar, entonces ~ C también puede hacerlo. así como ~ B y ~ A.
Con este simple ejemplo, tiene 4 destructores que fallaron en el "mismo momento" (salir del alcance). Lo que necesita no es una trampa con una excepción, sino una trampa con una serie de excepciones (esperemos que el código generado para esto no ... eh ... arroje).
catch(const std::vector<MyResourceException> & e)
{
// Do something with the vector of exceptions...
// Let''s hope if was not caused by an out-of-memory problem
}
Retomemos ( me gusta esta música ... ): cada excepción lanzada es diferente ( porque la causa es diferente: recuerde que en C ++, las excepciones no se derivan de std :: exception ). Ahora, necesita manejar simultáneamente cuatro excepciones. ¿Cómo podría escribir las cláusulas de captura manejando las cuatro excepciones por sus tipos, y por el orden en que fueron lanzadas?
¿Y qué pasa si tiene múltiples excepciones del mismo tipo, lanzadas por una desasignación múltiple y fallida? ¿Y qué ocurre si al asignar la memoria de las matrices de excepciones de las matrices, su programa se queda sin memoria y, por ejemplo, arroja una excepción de falta de memoria?
¿Estás seguro de que deseas dedicar tiempo a este tipo de problema en lugar de gastarlo pensando por qué la desasignación falló o cómo reaccionar de otra manera?
Al parecer, los diseñadores de C ++ no vieron una solución viable y simplemente redujeron sus pérdidas allí.
El problema no es RAII vs Excepciones ...
No, el problema es que a veces las cosas pueden fallar tanto que no se puede hacer nada.
RAII funciona bien con Excepciones, siempre que se cumplan algunas condiciones. Entre ellos: los destructores no lanzarán . Lo que está viendo como una oposición es solo un caso de esquina de un patrón único que combina dos "nombres": Excepción y RAII
En caso de que ocurra un problema en el destructor, debemos aceptar la derrota y salvar lo que se puede salvar : "¿No se pudo desasignar la conexión de DB? Lo siento, al menos evitemos esta pérdida de memoria y cierremos este archivo".
Mientras que el patrón de excepción es (supuestamente) el tratamiento principal de errores en C ++, no es el único. Debería manejar casos excepcionales (de carácter intencional) cuando las excepciones de C ++ no son una solución, mediante el uso de otros mecanismos de error / registro.
Debido a que acabas de encontrar un muro en el idioma, un muro que ningún otro idioma que conozco u oí pasó correctamente sin derribar la casa (el intento de C # valió la pena, mientras que el de Java sigue siendo una broma que me lastima de lado ... Ni siquiera hablaré sobre los lenguajes de scripting que fallarán en el mismo problema de la misma manera silenciosa).
Pero al final, no importa la cantidad de código que escriba, no estará protegido por el usuario que apaga la computadora .
Lo mejor que puedes hacer, ya lo has escrito. Mi propia preferencia es con un método de finalización arrojando, un destructor que no arroja recursos de limpieza no finalizados manualmente, y el log / messagebox (si es posible) para alertar sobre la falla en el destructor.
Tal vez no estás poniendo el duelo correcto. En lugar de "RAII vs. Excepción", debería ser " Intentar liberar recursos vs. Recursos que no desean ser liberados, incluso cuando están amenazados por la destrucción "
:-)
Puede saber si actualmente hay una excepción en el vuelo (por ejemplo, estamos entre el bloque throw y catch ejecutando el desenrollado de la pila, tal vez copiando objetos de excepción, o similares) verificando
bool std::uncaught_exception()
Si devuelve verdadero, tirar en este punto terminará el programa. Si no, es seguro lanzar (o al menos tan seguro como siempre). Esto se discute en la Sección 15.2 y 15.5.3 de ISO 14882 (estándar de C ++).
Esto no responde a la pregunta de qué hacer cuando aparece un error al limpiar una excepción, pero realmente no hay ninguna buena respuesta para eso. Pero sí le permite distinguir entre la salida normal y la salida excepcional si espera hacer algo diferente (como registrar e ignorarlo) en el último caso, en lugar de simplemente tomar pan caliente.
Una cosa que preguntaría es, ignorando la cuestión de la terminación y demás, ¿cuál cree que es una respuesta adecuada si su programa no puede cerrar su conexión DB, ya sea debido a la destrucción normal o la destrucción excepcional.
Parece que descarta "simplemente iniciar sesión" y no está dispuesto a terminar, entonces, ¿qué cree que es lo mejor que puede hacer?
Creo que si tuviéramos una respuesta a esa pregunta, entonces tendríamos una mejor idea de cómo proceder.
Ninguna estrategia me parece particularmente obvia; aparte de todo lo demás, realmente no sé lo que significa cerrar una conexión de base de datos para lanzar. ¿Cuál es el estado de la conexión si close () arroja? ¿Está cerrado, abierto o indeterminado? Y si es indeterminado, ¿hay alguna forma de que el programa vuelva a un estado conocido?
Un error del destructor significa que no había forma de deshacer la creación de un objeto; la única forma de devolver el programa a un estado conocido (seguro) es derribar todo el proceso y comenzar de nuevo.
NO DEBERÍA lanzar una excepción desde un destructor.
Nota: Actualizado para referirse a los cambios en el estándar:
En C ++ 03
Si una excepción ya se está propagando, la aplicación terminará.
En C ++ 11
Si el destructor es noexcept
(el valor predeterminado), la aplicación finalizará.
Lo siguiente se basa en C ++ 11
Si una excepción se escapa de una función noexcept
, se define su implementación si la pila está incluso desenrollada.
Lo siguiente se basa en C ++ 03
Al terminar, quiero decir parar inmediatamente. El desenrollado de la pila se detiene. No más destructores son llamados. Todas las cosas malas Vea la discusión aquí.
No sigo (en desacuerdo con) su lógica de que esto hace que el destructor se vuelva más complicado.
Con el uso correcto de punteros inteligentes, esto simplifica el destructor ya que todo se convierte automáticamente en automático. Cada clase marea su propia pequeña pieza del rompecabezas. No hay cirugía cerebral o ciencia de cohetes aquí. Otra gran victoria para RAII.
En cuanto a la posibilidad de std :: uncaught_exception () te señalo en el artículo de Herb Sutters sobre por qué no funciona