La mayoría de la gente dice que nunca arroje una excepción de un destructor, ya que esto da como resultado un comportamiento indefinido. Stroustrup señala que "el destructor de vectores invoca explícitamente el destructor para cada elemento. Esto implica que si un destructor de elementos arroja, la destrucción del vector falla ... Realmente no hay una buena manera de protegerse contra las excepciones lanzadas por los destructores, por lo que la biblioteca no garantiza si un destructor de elementos arroja "(del Apéndice E3.2) .
Este artículo parece decir lo contrario: arrojar destructores está más o menos bien.
Entonces mi pregunta es esta: si arrojar desde un destructor da como resultado un comportamiento indefinido, ¿cómo maneja los errores que ocurren durante un destructor?
Si se produce un error durante una operación de limpieza, ¿simplemente lo ignora? Si se trata de un error que potencialmente se puede manejar en la pila pero no en el destructor, ¿no tiene sentido descartar una excepción del destructor?
Obviamente este tipo de errores son raros, pero posibles.
fuente
xyz()
y mantén el destructor limpio de lógica que no sea RAII.commit()
que se llame a un método.Respuestas:
Lanzar una excepción de un destructor es peligroso.
Si ya se está propagando otra excepción, la aplicación finalizará.
Esto básicamente se reduce a:
Cualquier cosa peligrosa (es decir, que podría generar una excepción) debe hacerse a través de métodos públicos (no necesariamente directamente). El usuario de su clase puede manejar estas situaciones utilizando los métodos públicos y detectando posibles excepciones.
El destructor luego terminará el objeto llamando a estos métodos (si el usuario no lo hizo explícitamente), pero cualquier excepción arrojada se captura y se cae (después de intentar solucionar el problema).
En efecto, usted transfiere la responsabilidad al usuario. Si el usuario está en condiciones de corregir excepciones, llamará manualmente a las funciones apropiadas y procesará cualquier error. Si el usuario del objeto no está preocupado (ya que el objeto será destruido), entonces el destructor se ocupará de los negocios.
Un ejemplo:
std :: fstream
El método close () puede potencialmente lanzar una excepción. El destructor llama a close () si el archivo se ha abierto, pero se asegura de que las excepciones no se propaguen fuera del destructor.
Entonces, si el usuario de un objeto de archivo desea hacer un manejo especial para los problemas asociados con el cierre del archivo, llamará manualmente a close () y manejará cualquier excepción. Si, por otro lado, no les importa, entonces el destructor quedará a cargo de la situación.
Scott Myers tiene un excelente artículo sobre el tema en su libro "Effective C ++"
Editar:
Aparentemente también en "C ++ más eficaz"
Elemento 11: evitar que las excepciones dejen destructores
fuente
La eliminación de un destructor puede provocar un bloqueo, ya que este destructor podría llamarse como parte de "Despliegue de pila". El desenrollado de la pila es un procedimiento que tiene lugar cuando se produce una excepción. En este procedimiento, todos los objetos que fueron empujados a la pila desde el "intento" y hasta que se lanzó la excepción, serán terminados -> se llamará a sus destructores. Y durante este procedimiento, no se permite otro lanzamiento de excepción, ya que no es posible manejar dos excepciones a la vez, por lo tanto, esto provocará una llamada a abortar (), el programa se bloqueará y el control volverá al sistema operativo.
fuente
throw
perocatch
todavía no ha encontrado un bloque) en cuyo caso se invocastd::terminate
(noabort
) en lugar de generar una (nueva) excepción (o continuar el desbobinado de la pila).Tenemos que diferenciar aquí en lugar de seguir ciegamente los consejos generales para casos específicos .
Tenga en cuenta que a continuación se ignora el problema de los contenedores de objetos y qué hacer frente a múltiples archivos de objetos dentro de los contenedores. (Y puede ignorarse parcialmente, ya que algunos objetos simplemente no son adecuados para colocar en un contenedor).
Es más fácil pensar en todo el problema cuando dividimos las clases en dos tipos. Un dtor de clase puede tener dos responsabilidades diferentes:
Si vemos la pregunta de esta manera, entonces creo que se puede argumentar que la semántica (R) nunca debería causar una excepción de un dtor ya que hay a) nada que podamos hacer al respecto yb) muchas operaciones de recursos libres no incluso prever la comprobación de errores, por ejemplo .
void
free(void* p);
Los objetos con semántica (C), como un objeto de archivo que necesita vaciar con éxito sus datos o una conexión de base de datos ("alcance guardado") que realiza una confirmación en el dtor son de un tipo diferente: podemos hacer algo sobre el error (en el nivel de aplicación) y realmente no deberíamos continuar como si nada hubiera pasado.
Si seguimos la ruta RAII y permitimos los objetos que tienen semántica (C) en sus diseños, creo que también tenemos que tener en cuenta el caso extraño en el que pueden lanzar dichos diseños. De ello se deduce que no debe colocar tales objetos en contenedores y también se deduce que el programa aún puede
terminate()
hacerlo si un confirmador lanza mientras otra excepción está activa.Con respecto al manejo de errores (semántica Commit / Rollback) y excepciones, Andrei Alexandrescu habla bien : Manejo de errores en C ++ / Flujo de control declarativo (celebrado en NDC 2014 )
En los detalles, explica cómo la biblioteca Folly implementa un
UncaughtExceptionCounter
para susScopeGuard
herramientas.(Debo señalar que otros también tenían ideas similares).
Si bien la charla no se enfoca en lanzar desde un d'tor, muestra una herramienta que se puede usar hoy para deshacerse de los problemas con cuándo lanzar desde un d'tor.
En el
futuro, nopuedeser una característica std para esto,ver N3614 ,y una discusión al respecto .Upd '17: La característica estándar de C ++ 17 para esto es
std::uncaught_exceptions
afaikt. Citaré rápidamente el artículo de cppref:fuente
finally
.finally
es un dtor. Siempre se llama, pase lo que pase. Para una aproximación sintáctica de finalmente, vea las diversas implementaciones de scope_guard. Hoy en día, con la maquinaria en su lugar (incluso en el estándar, ¿es C ++ 14?) Para detectar si el dtor puede lanzar, incluso se puede hacer totalmente seguro.finally
es inherentemente una herramienta para (C). Si no ve por qué: considere por qué es legítimo lanzar excepciones una encima de la otra enfinally
bloques, y por qué lo mismo no es para destructores. (En cierto sentido, se trata de un control frente a los datos . Los destructores lo son para la liberación de los datos,finally
es para liberar el control Ellos son diferentes, es lamentable que en C ++ lazos juntos..)La verdadera pregunta que debe hacerse sobre lanzar desde un destructor es "¿Qué puede hacer la persona que llama con esto?" ¿Existe realmente algo útil que pueda hacer con la excepción, que compensaría los peligros creados al arrojar desde un destructor?
Si destruyo un
Foo
objeto y elFoo
destructor arroja una excepción, ¿qué puedo hacer razonablemente con él? Puedo registrarlo o puedo ignorarlo. Eso es todo. No puedo "arreglarlo" porque elFoo
objeto ya no está. En el mejor de los casos, registro la excepción y continúo como si nada hubiera pasado (o finalizo el programa). ¿Realmente vale la pena causar un comportamiento indefinido al lanzar desde un destructor?fuente
std::ofstream
El destructor se descarga y luego cierra el archivo. Se puede producir un error de disco lleno durante el vaciado, con lo que puede hacer algo útil: mostrar al usuario un cuadro de diálogo de error que dice que el disco no tiene espacio libre.Es peligroso, pero tampoco tiene sentido desde el punto de vista de la legibilidad / comprensión del código.
Lo que tienes que preguntar es en esta situación.
¿Qué debería atrapar la excepción? ¿La persona que llama de foo? ¿O debería manejarlo? ¿Por qué la persona que llama a foo debe preocuparse por algún objeto interno de foo? Puede haber una forma en que el lenguaje defina esto para que tenga sentido, pero será ilegible y difícil de entender.
Más importante aún, ¿a dónde va la memoria para Object? ¿A dónde va la memoria que posee el objeto? ¿Todavía está asignado (aparentemente porque el destructor falló)? Considere también que el objeto estaba en el espacio de la pila , por lo que obviamente desapareció independientemente.
Entonces considere este caso
Cuando falla la eliminación de obj3, ¿cómo elimino de una manera que garantice que no fallará? Es mi memoria maldita sea!
Ahora considere en el primer fragmento de código Object desaparece automáticamente porque está en la pila mientras Object3 está en el montón. Como el puntero a Object3 se ha ido, eres una especie de SOL. Tienes una pérdida de memoria.
Ahora, una forma segura de hacer las cosas es la siguiente
Consulte también estas preguntas frecuentes
fuente
int foo()
, puedes usar un bloque function-try-para envolver toda la función foo en un bloque try-catch, incluyendo la captura de destructores, si es que quieres hacerlo. Todavía no es el enfoque preferido, pero es una cosa.Del borrador ISO para C ++ (ISO / IEC JTC 1 / SC 22 N 4411)
Por lo tanto, los destructores generalmente deberían detectar excepciones y no dejar que se propaguen fuera del destructor.
fuente
Su destructor podría estar ejecutándose dentro de una cadena de otros destructores. Lanzar una excepción que no sea detectada por su interlocutor inmediato puede dejar múltiples objetos en un estado inconsistente, causando aún más problemas que ignorar el error en la operación de limpieza.
fuente
Estoy en el grupo que considera que el patrón de "protección con alcance" que arroja el destructor es útil en muchas situaciones, particularmente para pruebas unitarias. Sin embargo, tenga en cuenta que en C ++ 11, arrojar un destructor da como resultado una llamada a
std::terminate
ya que los destructores están anotados implícitamentenoexcept
.Andrzej Krzemieński tiene una gran publicación sobre el tema de los destructores que arrojan:
Señala que C ++ 11 tiene un mecanismo para anular el valor predeterminado
noexcept
para los destructores:Finalmente, si decide lanzar el destructor, siempre debe tener en cuenta el riesgo de una doble excepción (lanzar mientras la pila se desenrolla debido a una excepción). Esto provocaría una llamada
std::terminate
y rara vez es lo que desea. Para evitar este comportamiento, simplemente puede verificar si ya hay una excepción antes de lanzar una nueva usandostd::uncaught_exception()
.fuente
Todos los demás han explicado por qué arrojar destructores es terrible ... ¿qué pueden hacer al respecto? Si está realizando una operación que puede fallar, cree un método público separado que realice la limpieza y pueda generar excepciones arbitrarias. En la mayoría de los casos, los usuarios ignorarán eso. Si los usuarios desean monitorear el éxito / fracaso de la limpieza, simplemente pueden llamar a la rutina de limpieza explícita.
Por ejemplo:
fuente
Como complemento a las respuestas principales, que son buenas, completas y precisas, me gustaría comentar sobre el artículo al que hace referencia: el que dice "lanzar excepciones en los destructores no es tan malo".
El artículo toma la línea "cuáles son las alternativas a lanzar excepciones" y enumera algunos problemas con cada una de las alternativas. Una vez hecho esto, concluye que debido a que no podemos encontrar una alternativa sin problemas, debemos seguir lanzando excepciones.
El problema es que ninguno de los problemas que enumera con las alternativas es tan malo como el comportamiento de excepción, que, recordemos, es el "comportamiento indefinido de su programa". Algunas de las objeciones del autor incluyen "estéticamente feo" y "fomentar el mal estilo". ¿Ahora cuál preferirías tener? ¿Un programa con mal estilo, o uno que exhibió un comportamiento indefinido?
fuente
A: hay varias opciones:
Deje que las excepciones fluyan de su destructor, independientemente de lo que esté sucediendo en otro lugar. Y al hacerlo, tenga en cuenta (o incluso temeroso) que puede seguir std :: terminate.
Nunca permita que la excepción fluya de su destructor. Puede escribir en un registro, algún texto rojo grande y malo si puede.
mi favorito : si
std::uncaught_exception
devuelve falso, deja que fluyan excepciones. Si devuelve verdadero, vuelva al enfoque de registro.Pero, ¿es bueno tirar dorsos?
Estoy de acuerdo con la mayoría de lo anterior en que es mejor evitar tirar en el destructor, donde puede ser. Pero a veces es mejor aceptar que puede suceder y manejarlo bien. Yo elegiría 3 arriba.
Hay algunos casos extraños en los que en realidad es una gran idea lanzar desde un destructor. Al igual que el código de error "debe verificar". Este es un tipo de valor que se devuelve desde una función. Si la persona que llama lee / verifica el código de error contenido, el valor devuelto se destruye silenciosamente. Pero , si el código de error devuelto no se ha leído para el momento en que los valores devueltos están fuera de alcance, arrojará alguna excepción, desde su destructor .
fuente
Actualmente sigo la política (que muchos dicen) de que las clases no deberían lanzar activamente excepciones de sus destructores, sino que deberían proporcionar un método público de "cierre" para realizar la operación que podría fallar ...
... pero creo que los destructores para clases de tipo contenedor, como un vector, no deberían enmascarar las excepciones lanzadas desde las clases que contienen. En este caso, en realidad uso un método "libre / cerrado" que se llama a sí mismo de forma recursiva. Sí, dije recursivamente. Hay un método para esta locura. La propagación de excepciones depende de que haya una pila: si se produce una sola excepción, los dos destructores restantes se seguirán ejecutando y la excepción pendiente se propagará una vez que regrese la rutina, lo cual es excelente. Si ocurren varias excepciones, entonces (dependiendo del compilador) esa primera excepción se propagará o el programa terminará, lo cual está bien. Si se producen tantas excepciones que la recursión desborda la pila, entonces algo está muy mal y alguien lo descubrirá, lo que también está bien. Personalmente,
El punto es que el contenedor permanece neutral, y depende de las clases contenidas decidir si se comportan o se portan mal con respecto a lanzar excepciones de sus destructores.
fuente
A diferencia de los constructores, donde lanzar excepciones puede ser una forma útil de indicar que la creación de objetos tuvo éxito, las excepciones no deben arrojarse en destructores.
El problema se produce cuando se produce una excepción desde un destructor durante el proceso de desenrollado de la pila. Si eso sucede, el compilador se coloca en una situación en la que no sabe si continuar el proceso de desenrollado de la pila o manejar la nueva excepción. El resultado final es que su programa finalizará de inmediato.
En consecuencia, el mejor curso de acción es abstenerse de usar excepciones en destructores por completo. En su lugar, escriba un mensaje en un archivo de registro.
fuente
Martin Ba (arriba) está en el camino correcto: su arquitecto es diferente para la lógica de LIBERACIÓN y COMPROMISO.
Para lanzamiento:
Deberías comerte cualquier error. Estás liberando memoria, cerrando conexiones, etc. Nadie más en el sistema debería volver a VER esas cosas, y estás devolviendo recursos al sistema operativo. Si parece que necesita un manejo real de errores aquí, es probable que sea una consecuencia de fallas de diseño en su modelo de objetos.
Para comprometerse:
Aquí es donde desea el mismo tipo de objetos de contenedor RAII que cosas como std :: lock_guard proporcionan para mutexes. Con esos no se pone la lógica de confirmación en el dtor en absoluto. Tiene una API dedicada para ello, luego los objetos de envoltura que RAII lo confirmarán en SUS DTOR y manejarán los errores allí. Recuerde, puede atrapar excepciones en un destructor muy bien; emitirlos es mortal. Esto también le permite implementar políticas y diferentes manejos de errores simplemente construyendo un contenedor diferente (por ejemplo, std :: unique_lock vs. std :: lock_guard), y asegura que no olvidará llamar a la lógica de confirmación, que es la única mitad de camino justificación decente para ponerlo en un dtor en primer lugar.
fuente
El principal problema es este: no puedes dejar de fallar . ¿Qué significa fallar en fallar, después de todo? Si la confirmación de una transacción a una base de datos falla y falla (falla al deshacer), ¿qué sucede con la integridad de nuestros datos?
Dado que los destructores se invocan para rutas normales y excepcionales (falla), ellos mismos no pueden fallar o de lo contrario estamos "fallando en fallar".
Este es un problema conceptualmente difícil, pero a menudo la solución es encontrar una manera de asegurarse de que la falla no pueda fallar. Por ejemplo, una base de datos puede escribir cambios antes de comprometerse a una estructura de datos externa o archivo. Si la transacción falla, entonces la estructura de archivos / datos puede descartarse. Todo lo que tiene que asegurarse es que la confirmación de los cambios desde esa estructura / archivo externo sea una transacción atómica que no puede fallar.
La solución más adecuada para mí es escribir su lógica de no limpieza de tal manera que la lógica de limpieza no pueda fallar. Por ejemplo, si tiene la tentación de crear una nueva estructura de datos para limpiar una estructura de datos existente, entonces tal vez debería intentar crear esa estructura auxiliar por adelantado para que ya no tengamos que crearla dentro de un destructor.
Todo esto es mucho más fácil decirlo que hacerlo, es cierto, pero es la única forma realmente adecuada que veo de hacerlo. A veces creo que debería haber una capacidad para escribir una lógica de destructor separada para rutas de ejecución normales lejos de las excepcionales, ya que a veces los destructores sienten que tienen el doble de responsabilidades al tratar de manejar ambos (un ejemplo son los protectores de alcance que requieren un rechazo explícito) ; no requerirían esto si pudieran diferenciar los caminos de destrucción excepcionales de los no excepcionales).
Aún así, el problema final es que no podemos dejar de fallar, y es un problema de diseño conceptual difícil de resolver perfectamente en todos los casos. Se vuelve más fácil si no se envuelve demasiado en estructuras de control complejas con toneladas de pequeños objetos que interactúan entre sí, y en su lugar modela sus diseños de una manera un poco más voluminosa (ejemplo: sistema de partículas con un destructor para destruir toda la partícula sistema, no un destructor no trivial separado por partícula). Cuando modela sus diseños en este tipo de nivel más grueso, tiene menos destructores no triviales con los que lidiar y, a menudo, también puede permitirse cualquier gasto de memoria / procesamiento necesario para asegurarse de que sus destructores no puedan fallar.
Y esa es una de las soluciones más fáciles, naturalmente, es usar destructores con menos frecuencia. En el ejemplo de partículas anterior, tal vez al destruir / eliminar una partícula, se deben hacer algunas cosas que podrían fallar por cualquier razón. En ese caso, en lugar de invocar dicha lógica a través del dtor de la partícula que podría ejecutarse en una ruta excepcional, podría hacer que todo lo haga el sistema de partículas cuando elimine una partícula. La eliminación de una partícula siempre se puede hacer durante un camino no excepcional. Si el sistema se destruye, tal vez pueda purgar todas las partículas y no molestarse con esa lógica de eliminación de partículas individual que puede fallar, mientras que la lógica que puede fallar solo se ejecuta durante la ejecución normal del sistema de partículas cuando está eliminando una o más partículas.
A menudo hay soluciones como esa que surgen si evitas lidiar con muchos objetos pequeños con destructores no triviales. Donde puede enredarse en un desastre donde parece casi imposible ser una excepción, la seguridad es cuando se enreda en muchos objetos pequeños que tienen dtors no triviales.
Sería de gran ayuda si nothrow / noexcept se tradujera realmente en un error del compilador si algo que lo especifica (incluidas las funciones virtuales que deberían heredar la especificación noexcept de su clase base) intentara invocar cualquier cosa que pudiera arrojar. De esta manera, podríamos atrapar todo esto en tiempo de compilación si en realidad escribimos un destructor inadvertidamente que podría arrojar.
fuente
Establecer un evento de alarma. Por lo general, los eventos de alarma son una mejor forma de notificar fallas mientras se limpian objetos
fuente