arrojando excepciones de un destructor

La mayoría de la gente dice que nunca arroje una excepción de un destructor, lo que da como resultado un comportamiento indefinido. Stroustrup señala que “el vector destructor invoca explícitamente el destructor para cada elemento. Esto implica que si un elemento destructor arroja, la destrucción del vector falla … Realmente no hay una buena forma de protegerse contra las excepciones lanzadas desde los destructores, por lo que la biblioteca no garantiza si un elemento destructor arroja “(del Apéndice E3.2) .

Este artículo parece decir lo contrario: que los destructores arrojados están más o menos bien.

Entonces mi pregunta es esta: si lanzar desde un destructor resulta en un comportamiento indefinido, ¿cómo manejas los errores que ocurren durante un destructor?

Si ocurre un error durante una operación de limpieza, ¿simplemente lo ignora? Si se trata de un error que potencialmente se puede manejar en la stack pero no en el destructor, ¿no tiene sentido lanzar una excepción desde el destructor?

Obviamente, este tipo de errores son raros, pero posibles.

Lanzar una excepción de un destructor es peligroso.
Si otra excepción ya se está propagando, la aplicación terminará.

 #include  class Bad { public: // Added the noexcept(false) so the code keeps its original meaning. // Post C++11 destructors are by default `noexcept(true)` and // this will (by default) call terminate if an exception is // escapes the destructor. // // But this example is designed to show that terminate is called // if two exceptions are propagating at the same time. ~Bad() noexcept(false) { throw 1; } }; class Bad2 { public: ~Bad2() { throw 1; } }; int main(int argc, char* argv[]) { try { Bad bad; } catch(...) { std::cout << "Print This\n"; } try { if (argc > 3) { Bad bad; // This destructor will throw an exception that escapes (see above) throw 2; // But having two exceptions propagating at the // same time causes terminate to be called. } else { Bad2 bad; // The exception in this destructor will // cause terminate to be called. } } catch(...) { std::cout << "Never print this\n"; } } 

Esto básicamente se reduce a:

Cualquier cosa peligrosa (es decir, que podría arrojar una excepción) debe hacerse a través de métodos públicos (no necesariamente directamente). El usuario de su clase puede manejar potencialmente estas situaciones utilizando los métodos públicos y atrapando cualquier posible excepción.

El destructor terminará el objeto llamando a estos métodos (si el usuario no lo hizo explícitamente), pero cualquier lanzamiento de excepciones se detecta y se descarta (después de intentar solucionar el problema).

Entonces, en efecto, le pasas la responsabilidad al usuario. Si el usuario está en condiciones de corregir excepciones, invocará manualmente las funciones apropiadas y procesará cualquier error. Si el usuario del objeto no está preocupado (ya que el objeto será destruido), entonces el destructor quedará encargado de los negocios.

Un ejemplo:

std :: fstream

El método close () puede arrojar una excepción. El destructor llama a close () si el archivo se ha abierto pero se asegura de que ninguna excepción se propague desde el destructor.

Por lo tanto, si el usuario de un objeto de archivo desea realizar 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á para manejar 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 efectivo"
Punto 11: evitar que las excepciones salgan de los destructores

Lanzar un destructor puede provocar un locking, ya que este destructor podría ser llamado como parte de “Desenrollado de stack”. El desenrollado de la stack es un procedimiento que tiene lugar cuando se lanza una excepción. En este procedimiento, todos los objetos que se insertaron en la stack desde el “bash” y hasta que se arrojó la excepción, finalizarán -> se invocarán sus destructores. Y durante este procedimiento, no se permite otro lanzamiento de excepción, porque no es posible manejar dos excepciones a la vez, por lo tanto, esto provocará una llamada a abort (), el progtwig se bloqueará y el control volverá al sistema operativo.

Tenemos que diferenciar aquí en lugar de seguir ciegamente los consejos generales para casos específicos .

Tenga en cuenta que lo siguiente ignora el problema de los contenedores de objetos y qué hacer frente a múltiples objetos dentro de los contenedores. (Y se puede ignorar parcialmente, ya que algunos objetos simplemente no son aptos para colocar en un contenedor).

Todo el problema se vuelve más fácil de pensar cuando dividimos las clases en dos tipos. Un dtor de clase puede tener dos responsabilidades diferentes:

  • (R) libera semántica (también conocida como liberar esa memoria)
  • (C) semántica de commit (también conocido como flush file to disk)

Si vemos la pregunta de esta manera, entonces creo que se puede argumentar que la semántica (R) nunca debe causar una excepción de un dtor ya que hay a) nada que podamos hacer al respecto yb) muchas operaciones de recursos gratuitos no incluso proporcionar verificación de errores, por ejemplo, free(void* p); .

Los objetos con semántica (C), como un objeto de archivo que necesita eliminar correctamente sus datos o una conexión de base de datos (“protegido por scope”) que realiza una confirmación en el controlador son de un tipo diferente: podemos hacer algo con el error (en el nivel de aplicación) y realmente no deberíamos continuar como si nada hubiera sucedido.

Si seguimos la ruta RAII y permitimos que los objetos tengan semántica (C) en sus direcciones, creo que también debemos tener en cuenta el extraño caso en el que dichos motores pueden arrojar. De esto se deduce que no debe poner tales objetos en contenedores y también se deduce que el progtwig aún puede terminate() si un commit-dtor lanza mientras está activa otra excepción.


Con respecto al manejo de errores (semántica de Compromiso / Reversión) y excepciones, hay una buena charla por parte de Andrei Alexandrescu : Manejo de errores en C ++ / flujo de control declarativo (celebrada en NDC 2014 )

En los detalles, explica cómo la biblioteca Folly implementa UncaughtExceptionCounter para su herramienta ScopeGuard .

(Debo señalar que los demás también tenían ideas similares).

Si bien la charla no se centra en arrojar desde un toro, se muestra una herramienta que se puede utilizar hoy para deshacerse de los problemas de cuándo tirar de un toro.

En el futuro , puede haber una característica estándar 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 cppref:

Notas

Un ejemplo donde int -returnundecaught_exceptions es usado … … primero crea un objeto guard y registra el número de excepciones no detectadas en su constructor. La salida la realiza el destructor del objeto guard a menos que foo () lance ( en cuyo caso el número de excepciones no detectadas en el destructor es mayor que lo que el constructor observó )

La verdadera pregunta para preguntarse acerca de tirar desde un destructor es “¿Qué puede hacer la persona que llama con esto?” ¿Hay realmente algo útil que puedas hacer con la excepción, que compensaría los peligros creados al tirar desde un destructor?

Si destruyo un objeto Foo , y el destructor Foo arroja una excepción, ¿qué puedo hacer razonablemente con eso? Puedo iniciar sesión o puedo ignorarlo. Eso es todo. No puedo “arreglarlo”, porque el objeto Foo ya se ha ido. En el mejor de los casos, registro la excepción y continúo como si nada hubiera sucedido (o finalizo el progtwig). ¿Realmente vale la pena causar un comportamiento indefinido arrojándolo desde un destructor?

Es peligroso, pero tampoco tiene sentido desde el punto de vista de legibilidad / comprensión del código.

Lo que tienes que preguntar es en esta situación

 int foo() { Object o; // As foo exits, o's destructor is called } 

¿Qué debería atrapar la excepción? ¿Debería la persona que llama de foo? ¿O deberíamos manejarlo? ¿Por qué la persona que llama a foo se preocupa por algún objeto interno para foo? Puede haber una manera en que el lenguaje lo define para tener sentido, pero será ilegible y difícil de entender.

Más importante aún, ¿a dónde va la memoria para Object? ¿De dónde va la memoria que posee el objeto? ¿Todavía está asignado (ostensiblemente porque el destructor falló)? Considere también que el objeto estaba en el espacio de la stack , por lo que obviamente desapareció.

Entonces considera este caso

 class Object { Object2 obj2; Object3* obj3; virtual ~Object() { // What should happen when this fails? How would I actually destroy this? delete obj3; // obj 2 fails to destruct when it goes out of scope, now what!?!? // should the exception propogate? } }; 

Cuando falla la eliminación de obj3, ¿cómo puedo eliminar de una manera que garantice que no fallará? Es mi memoria maldita sea!

Ahora considere en el primer fragmento de código El objeto desaparece automáticamente porque está en la stack mientras que Object3 está en el montón. Dado que el puntero a Object3 se ha ido, eres como SOL. Usted tiene una pérdida de memoria.

Ahora una forma segura de hacer las cosas es la siguiente

 class Socket { virtual ~Socket() { try { Close(); } catch (...) { // Why did close fail? make sure it *really* does close here } } }; 

Ver también estas preguntas frecuentes

Del borrador ISO para C ++ (ISO / IEC JTC 1 / SC 22 N 4411)

Por lo tanto, los destructores generalmente deben detectar excepciones y no dejar que se propaguen desde el destructor.

3 El proceso de invocación de destructores para objetos automáticos construidos en la ruta desde un bloque try a una expresión throw se llama “desenrollado de stack”. [Nota: si un destructor llamado durante el desenrollado de stack sale con una excepción, se llama std :: terminate (15.5.1). Por lo tanto, los destructores generalmente deben detectar excepciones y no dejar que se propaguen desde el destructor. – nota final]

Su destructor podría estar ejecutándose dentro de una cadena de otros destructores. Lanzar una excepción que no haya sido capturada por su llamador inmediato puede dejar varios objetos en un estado incoherente, causando incluso más problemas y luego ignorando el error en la operación de limpieza.

Todos los demás han explicado por qué los destructores de lanzamiento son terribles … ¿qué puedes 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 lanzar 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:

 class TempFile { public: TempFile(); // throws if the file couldn't be created ~TempFile() throw(); // does nothing if close() was already called; never throws void close(); // throws if the file couldn't be deleted (eg file is open by another process) // the rest of the class omitted... }; 

Como complemento de las respuestas principales, que son buenas, exhaustivas 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 para lanzar excepciones” y enumera algunos problemas con cada una de las alternativas. Una vez hecho esto, concluye que, como no podemos encontrar una alternativa libre de problemas, debemos seguir lanzando excepciones.

El problema es que ninguno de los problemas que enumera con las alternativas es casi tan malo como el comportamiento de excepción, que, recordemos, es “comportamiento indefinido de su progtwig”. Algunas de las objeciones del autor incluyen “estéticamente feo” y “alentar el mal estilo”. ¿Ahora qué preferirías tener? ¿Un progtwig con mal estilo o que exhibió un comportamiento indefinido?

P: Entonces mi pregunta es esta: si lanzar desde un destructor resulta en un comportamiento indefinido, ¿cómo manejas los errores que ocurren durante un destructor?

A: hay varias opciones:

  1. Deje que las excepciones fluyan desde su destructor, independientemente de lo que esté pasando en otro lugar. Y al hacerlo, tenga en cuenta (o incluso temeroso) que std :: terminate puede seguir.

  2. Nunca dejes que la excepción salga de tu destructor. Puede escribir en un registro, algún gran mensaje de texto en rojo si es posible.

  3. mi favorito : si std::uncaught_exception devuelve falso, deja que salgan las excepciones. Si devuelve verdadero, vuelva al enfoque de registro.

¿Pero es bueno tirar entradas?

Estoy de acuerdo con la mayoría de los anteriores que es mejor evitar tirar en destructor, donde puede ser. Pero a veces es mejor que aceptes que puede suceder y lo manejes bien. Elegiría 3 arriba.

Hay algunos casos extraños en los que es realmente una gran idea lanzar desde un destructor. Como 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 en silencio. Pero , si el código de error devuelto no se ha leído antes de que los valores de retorno salgan del scope, arrojará alguna excepción desde su destructor .

Actualmente sigo la política (que muchos dicen) de que las clases no deberían arrojar excepciones de sus destructores, sino que deberían proporcionar un método público “cerrado” para realizar la operación que podría fallar …

… pero creo que los destructores para las clases de tipo contenedor, como un vector, no deberían enmascarar excepciones lanzadas desde las clases que contienen. En este caso, utilizo un método de “libre / cerrado” que se llama recursivamente. Sí, dije recursivamente. Hay un método para esta locura. La propagación de excepciones se basa en la existencia de una stack: si se produce una sola excepción, se seguirán ejecutando los destructores restantes y la excepción pendiente se propagará una vez que regrese la rutina, lo cual es excelente. Si ocurren múltiples excepciones, entonces (dependiendo del comstackdor), o bien la primera excepción se propagará o el progtwig terminará, lo cual está bien. Si ocurren tantas excepciones que la recursión sobrepasa la stack, algo está muy mal y alguien se va a enterar, lo que también está bien. Personalmente, me equivoco del lado de los errores que explotan en lugar de estar ocultos, secretos e insidiosos.

El punto es que el contenedor permanece neutral, y corresponde a las clases contenidas decidir si se comportan o se portan mal con respecto a arrojar excepciones de sus destructores.

Estoy en el grupo que considera que el patrón de “protector de scope” que arroja el destructor es útil en muchas situaciones, especialmente para pruebas unitarias. Sin embargo, tenga en cuenta que en C ++ 11, al lanzar un destructor se produce una llamada a std::terminate ya que los destructores están anotados implícitamente con noexcept .

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 no noexcept para los destructores:

En C ++ 11, un destructor se especifica implícitamente como noexcept . Incluso si no agrega ninguna especificación y define su destructor de esta manera:

  class MyType { public: ~MyType() { throw Exception(); } // ... }; 

El comstackdor todavía agregará invisiblemente la especificación noexcept a su destructor. Y esto significa que en el momento en que su destructor arroje una excepción, se llamará a std::terminate , incluso si no hubiera una situación de excepción doble. Si está realmente decidido a permitir el lanzamiento de sus destructores, deberá especificarlo explícitamente; tienes tres opciones:

  • Especifique explícitamente su destructor como noexcept(false) ,
  • Hereda tu clase de otra que ya especifique su destructor como noexcept(false) .
  • Coloque un miembro de datos no estáticos en su clase que ya especifique su destructor como noexcept(false) .

Finalmente, si decide lanzar el destructor, siempre debe tener en cuenta el riesgo de una doble excepción (lanzamiento mientras la stack se está desenrollando debido a una excepción). Esto causaría una llamada a std::terminate y rara vez es lo que quiere. Para evitar este comportamiento, simplemente puede verificar si ya hay una excepción antes de lanzar una nueva utilizando std::uncaught_exception() .

Establecer un evento de alarma. Normalmente, los eventos de alarma son una mejor forma de notificación de fallas al limpiar objetos.

A diferencia de los constructores, donde arrojar 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 ocurre cuando se lanza una excepción desde un destructor durante el proceso de desenrollado de la stack. Si eso sucede, el comstackdor se encuentra en una situación en la que no sabe si continuar el proceso de desenrollado de la stack o manejar la nueva excepción. El resultado final es que su progtwig terminará inmediatamente.

En consecuencia, el mejor curso de acción es simplemente abstenerse de usar excepciones en destructores por completo. Escriba un mensaje en un archivo de registro en su lugar.

Martin Ba (arriba) está en el camino correcto: usted es arquitecto de manera diferente para la lógica RELEASE y COMMIT.

Para publicación:

Deberías comer cualquier error. Estás liberando memoria, cerrando conexiones, etc. Nadie más en el sistema debería VER nunca más esas cosas, y estás devolviendo recursos al SO. Si parece que necesita un manejo de error real aquí, es probable que sea una consecuencia de defectos de diseño en su modelo de objeto.

Para Commit:

Aquí es donde desea el mismo tipo de objetos de envoltura RAII que cosas como std :: lock_guard proporcionan mutexes. Con aquellos, no pongas la lógica de compromiso en el dtor EN ABSOLUTO. Tienes una API dedicada para ella, luego objetos envoltorios que RAII lo comprometerá en SUS dtors y manejará los errores allí. Recuerde, puede capturar excepciones en un destructor sin problemas; su emisión es mortal. Esto también le permite implementar política y manejo de errores diferentes simplemente construyendo un contenedor diferente (por ejemplo, std :: unique_lock vs. std :: lock_guard), y asegura que no se olvide de llamar a la lógica de confirmación, que es la única a mitad de camino justificación decente para ponerlo en un dtor en el 1er lugar.

Entonces mi pregunta es esta: si lanzar desde un destructor resulta en un comportamiento indefinido, ¿cómo manejas los errores que ocurren durante un destructor?

El principal problema es este: no puedes dejar de fallar . ¿Qué significa no poder fallar, después de todo? Si falla la realización de una transacción en una base de datos, y no puede fallar (no puede retrotraerse), ¿qué ocurre con la integridad de nuestros datos?

Dado que los destructores son invocados para rutas normales y excepcionales (fallidas), 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 la forma de asegurarse de que la falla no pueda fallar. Por ejemplo, una base de datos puede escribir cambios antes de comprometerse con una estructura o archivo de datos externo. Si la transacción falla, entonces la estructura del archivo / datos se puede descartar. Todo lo que tiene que asegurar es que al comprometer los cambios desde esa estructura externa / archivo, una transacción atómica que no puede fallar.

La solución pragmática es, tal vez, solo asegurarse de que las posibilidades de fallar en el fracaso sean astronómicamente improbables, ya que hacer las cosas imposibles de fallar puede ser casi imposible en algunos casos.

La solución más adecuada para mí es escribir su lógica de no limpieza de forma tal 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 podría tratar de 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, hay que admitirlo, pero es la única manera realmente correcta de hacerlo. A veces pienso que debería existir la capacidad de escribir una lógica de destrucción separada para las rutas normales de ejecución lejos de las excepcionales, ya que a veces los destructores se sienten un poco como si tuvieran el doble de responsabilidades al tratar de manejar ambas (un ejemplo es guardias de scope que requieren expulsión explícita ; no lo requerirían si pudieran diferenciar las rutas de destrucción excepcionales de las no excepcionales).

Todavía 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 te envuelves en complejas estructuras de control con toneladas de pequeños objetos que interactúan entre sí, y en cambio modelas tus diseños de una manera un poco más voluminosa (ejemplo: sistema de partículas con un destructor para destruir la partícula completa) sistema, no un destructor separado no trivial por partícula). Cuando modelas tus diseños en este tipo de nivel más grueso, tienes menos destructores no triviales para tratar, y también puedes permitirte el gasto de memoria / procesamiento para asegurarte de que tus destructores no fallen.

Y esa es una de las soluciones más fáciles, naturalmente, es usar destructores con menos frecuencia. En el ejemplo de la partícula anterior, quizás al destruir / eliminar una partícula, se deberían hacer algunas cosas que podrían fallar por cualquier razón. En ese caso, en lugar de invocar tal lógica a través del dtor de la partícula que podría ejecutarse en un camino excepcional, en cambio podrías tenerlo todo hecho por el sistema de partículas cuando elimina una partícula. La eliminación de una partícula siempre se puede hacer durante una ruta no excepcional. Si el sistema se destruye, puede simplemente purgar todas las partículas y no molestarse con esa lógica individual de eliminación de partículas 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 elimina una o más partículas.

A menudo hay soluciones como esa que surgen si evitas tratar con muchos objetos pequeños con destructores no triviales. Donde te puedes enredar en un lío en el que parece casi imposible ser una excepción; la seguridad es cuando te enredas en muchos pequeños objetos que tienen dtors no triviales.

Ayudaría mucho si nothrow / noexcept realmente se traduce en un error de comstackción 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 arrojarse. De esta forma, podríamos capturar todo esto en tiempo de comstackción si escribimos un destructor inadvertidamente que podría arrojar.