barco acorazado accion c++ exception destructor raii

c++ - acorazado - lanzando excepciones de un destructor



accion exception (16)

Así que mi pregunta es la siguiente: si lanzar desde un destructor produce un comportamiento indefinido, ¿cómo maneja los errores que ocurren durante un destructor?

El principal problema es el siguiente: no puedes fallar . ¿Qué significa fallar, después de todo? Si la confirmación de una transacción en una base de datos falla y falla (no se puede revertir), ¿qué sucede con la integridad de nuestros datos?

Dado que los destructores se invocan 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 una manera de asegurarse de que la falla no pueda fallar. Por ejemplo, una base de datos puede escribir cambios antes de comprometerse con un archivo o estructura de datos externos. Si la transacción falla, la estructura del archivo / datos puede ser eliminada. Todo lo que tiene que asegurar es que al realizar los cambios desde esa estructura / archivo externo se realice 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 que las cosas sean 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 tal manera que la lógica de limpieza no pueda fallar. Por ejemplo, si está tentado a crear una nueva estructura de datos para limpiar una estructura de datos existente, entonces tal vez deba buscar crear esa estructura auxiliar de antemano 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 manera realmente apropiada para hacerlo. A veces creo que debería haber una capacidad para escribir una lógica de destructor separada para las 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 ambas (un ejemplo son las protecciones de alcance que requieren un despido explícito ; no requerirían esto si pudieran diferenciar caminos de destrucción excepcionales de los no excepcionales).

Aún así, el problema final es que no podemos 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 estructuras de control complejas con toneladas de objetos pequeños que interactúan entre sí, y en su lugar 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 modela sus diseños en este tipo de nivel más grueso, tiene que lidiar con menos destructores no triviales y, a menudo, también puede pagar la sobrecarga de memoria / procesamiento necesaria para asegurarse de que sus destructores no puedan fallar.

Y esa es una de las soluciones más fáciles, naturalmente, es usar los destructores con menos frecuencia. En el ejemplo de partículas anterior, quizás al destruir / eliminar una partícula, se deben hacer algunas cosas que podrían fallar por cualquier motivo. 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, en su lugar, el sistema de partículas lo puede hacer todo cuando elimina una partícula. La eliminación de una partícula siempre se puede hacer durante un camino no excepcional. Si se destruye el sistema, tal vez solo pueda 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 está eliminando 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 puede enredarse en un lío en el que parece casi imposible ser una excepción, la seguridad es cuando se enreda en muchos objetos pequeños que tienen dorsores no triviales.

Sería de gran ayuda si nothrow / noexcept se tradujera realmente a un error del compilador si cualquier cosa que lo especifique (incluidas las funciones virtuales que deberían heredar la especificación noexcept de su clase base) intentó invocar cualquier cosa que pudiera lanzar. De esta manera podríamos atrapar todo esto en tiempo de compilación si realmente escribimos un destructor inadvertidamente que podría lanzar.

La mayoría de la gente dice que nunca se debe sacar una excepción de un destructor, ya que esto resulta en 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 manera de protegerse contra las excepciones lanzadas por los destructores, por lo que la biblioteca no ofrece garantías si un destructor de elementos lanza "(del Apéndice E3.2) .

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

Entonces mi pregunta es esta: si lanzar desde un destructor produce 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 es un error que potencialmente puede ser manejado en la pila pero no en el destructor, ¿no tiene sentido lanzar una excepción fuera del destructor?

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


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

R: Hay varias opciones:

  1. Deje que las excepciones salgan de su destructor, independientemente de lo que ocurra en otro lugar. Y al hacerlo, tenga en cuenta (o incluso tenga miedo) que puede seguir std :: terminate.

  2. Nunca dejes que la excepción salga de tu destructor. Se puede escribir en un registro, un gran texto mal rojo si puedes.

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

¿Pero es bueno tirar en d''tors?

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

Hay algunos casos extraños en los que es realmente una gran idea lanzar desde un destructor. Al igual que el código de error "debe comprobar". 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 en el momento en que los valores devueltos están fuera del alcance, lanzará alguna excepción desde su destructor .


A diferencia de los constructores, donde lanzar excepciones puede ser una forma útil de indicar que la creación del objeto se realizó correctamente, las excepciones no deben lanzarse en los destructores.

El problema ocurre cuando se lanza 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 terminará inmediatamente.

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


Actualmente sigo la política (que muchos dicen) de que las clases no deberían lanzar excepciones de sus destructores de forma activa, 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 generadas por 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 aún se ejecutarán y la excepción pendiente se propagará una vez que la rutina regrese, lo que es excelente. Si se producen múltiples excepciones, entonces (dependiendo del compilador) o la primera excepción se propagará o el programa terminará, lo cual está bien. Si ocurren tantas excepciones que la recursión desborda la pila, entonces algo está seriamente mal y alguien lo descubrirá, lo cual también está bien. Personalmente, me equivoco en el lado de los errores que explotan en lugar de ser ocultos, secretos e insidiosos.

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


Como una adición 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 son tan malos 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?


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

Por lo tanto, los destructores generalmente deben capturar las excepciones y no dejar que se propaguen fuera del destructor.

3 El proceso de llamar a los destructores para los objetos automáticos construidos en la ruta desde un bloque de prueba a una expresión de lanzamiento se denomina "desenvolvimiento de pila". [Nota: si un destructor llamado durante el desenrollado de pila sale con una excepción, se llama std :: terminate (15.5.1). Por lo tanto, los destructores generalmente deben capturar las excepciones y no dejar que se propaguen fuera del destructor. - nota final]


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? ¿Debe la persona que llama de foo? ¿O debería foo manejarlo? ¿Por qué a la persona que llama foo le importa un objeto interno de foo? Puede haber una manera en que el lenguaje lo define para que tenga sentido, pero será ilegible y difícil de entender.

Más importante aún, ¿a dónde va la memoria de Object? ¿A dónde va la memoria del objeto que posee? ¿Sigue siendo asignado (aparentemente porque el destructor falló)? Considere también que el objeto estaba en el espacio de pila , por lo que, obviamente, se ha ido independientemente.

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 elimino de una manera que se garantiza que no fallará? Es mi memoria maldita sea!

Ahora considere en el primer fragmento de código que Object desaparece automáticamente porque está en la pila mientras que Object3 está en el montón. Ya que el puntero a Object3 se ha ido, estás como SOL. Tienes 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 } } };

También vea este FAQ


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


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

Andrzej Krzemieński tiene un gran post sobre el tema de los destructores que lanzan:

Señala que C ++ 11 tiene un mecanismo para anular el noexcept predeterminado 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 compilador todavía agregará invisiblemente la especificación noexcept a su destructor. Y esto significa que en el momento en que su destructor lanza una excepción, se llamará std::terminate , incluso si no hubiera una situación de doble excepción. Si realmente estás decidido a permitir que tus destructores lancen, deberás especificar esto explícitamente; Tienes tres opciones:

  • Especifique explícitamente su destructor como noexcept(false) ,
  • Hereda su 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 (el lanzamiento mientras la pila se está desenrollando debido a una excepción). Esto causaría una llamada a std::terminate y rara vez es lo que quieres. Para evitar este comportamiento, simplemente puede verificar si ya existe una excepción antes de lanzar una nueva usando std::uncaught_exception() .


La verdadera pregunta que debe hacerse acerca de lanzar 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 lanzar desde un destructor?

Si destruyo un objeto Foo y el destructor Foo lanza una excepción, ¿qué puedo hacer razonablemente con él? Puedo registrarlo, 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 no hubiera pasado nada (o finalice el programa). ¿Realmente vale la pena causar un comportamiento indefinido al lanzar desde un destructor?


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

#include <iostream> 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 lanzar una excepción) debe hacerse a través de métodos públicos (no necesariamente directamente). El usuario de su clase puede entonces potencialmente manejar estas situaciones usando los métodos públicos y detectando las 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 lanzada se captura y se cae (después de intentar solucionar el problema).

Entonces, en efecto, pasas la responsabilidad al usuario. Si el usuario está en posición de corregir las 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), se deja que el destructor se encargue 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.

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á las excepciones. Si, por otro lado, no les importa, 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 efectivo"
Ítem ​​11: Evitar que las excepciones salgan de los destructores.


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

Para su publicación:

Debes comer cualquier error. Estás liberando memoria, cerrando conexiones, etc. Nadie más en el sistema debería VER esas cosas otra vez, y estás devolviendo recursos al sistema operativo. Si parece que necesita un verdadero manejo de errores aquí, es probable que sea una consecuencia de fallas de diseño en su modelo de objeto.

Para Commit:

Aquí es donde desea el mismo tipo de objetos de envoltorio RAII que cosas como std :: lock_guard proporcionan para mutexes. Con esos no pones la lógica de cometer en el dtor AT ALL. Usted tiene una API dedicada para ello, luego los objetos de envoltura que RAII la confirmarán en SUS controladores y manejarán los errores allí. Recuerda, puedes capturar excepciones en un destructor muy bien; Su emisión es mortal. Esto también le permite implementar una política y un manejo diferente de los errores con solo construir un contenedor diferente (por ejemplo, std :: unique_lock vs. std :: lock_guard), y asegura que no se olvidará de llamar a la lógica de confirmación, que es la única a medio camino. Justificación decente para ponerlo en un dtor en el 1er lugar.


Sacar un destructor puede provocar una caída, ya que este destructor podría llamarse como parte de "Desenrollado de pila". El desenrollado de la pila es un procedimiento que tiene lugar cuando se lanza una excepción. En este procedimiento, todos los objetos que fueron empujados en 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 otra 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 regresará al sistema operativo.


Tenemos que diferenciarnos 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 almacenes de objetos dentro de los contenedores. (Y se puede ignorar parcialmente, ya que algunos objetos no son adecuados para colocarlos 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) liberar semántica (también conocido como free that memory)
  • (C) cometer semántica (también conocido como archivo de descarga al disco)

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 a) no podemos hacer nada al respecto yb) muchas operaciones de recursos libres no lo hacen. incluso prever la comprobación de errores, por ejemplo, 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 ("vigilada por alcance") que realiza una confirmación en el dtor son de un tipo diferente: podemos hacer algo al respecto (en el nivel de aplicación) y realmente no deberíamos continuar como si nada hubiera pasado.

Si seguimos la ruta RAII y permitimos que los objetos que tienen (C) semántica en sus d''tors, creo que también tenemos que tener en cuenta el extraño caso en el que dichos d''tors pueden lanzar. De ello se deduce que no debe colocar dichos objetos en contenedores y también que el programa aún puede terminate() si se lanza un indicador de compromiso mientras hay otra excepción activa.

Con respecto al manejo de errores (semántica de Commit / Rollback) y las excepciones, hay una buena charla de uno de Andrei Alexandrescu : Manejo de errores en C ++ / Flujo de control declarativo (realizado en NDC 2014 )

En los detalles, explica cómo la biblioteca de Folly implementa UncaughtExceptionCounter para sus herramientas de ScopeGuard .

(Debo notar que others también tuvieron ideas similares).

Si bien la conversación no se centra en lanzar desde un d''tor, muestra una herramienta que se puede utilizar hoy para deshacerse de los problemas con cuándo lanzar desde un d''tor.

En el futuro , puede haber una característica N3614 para esto, vea N3614 , y una discusión al respecto .

Actualización ''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 en el que se usa int -returning uncaught_exceptions es ... ... primero crea un objeto de guarda y registra el número de excepciones no detectadas en su constructor. La salida es realizada por el destructor del objeto guard a menos que foo () arroje ( en cuyo caso el número de excepciones no capturadas en el destructor es mayor que lo que observó el constructor )


Todos los demás han explicado por qué tirar los destructores es terrible ... ¿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 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:

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 (e.g. file is open by another process) // the rest of the class omitted... };


Tu 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 e ignorando el error en la operación de limpieza.