Pregunta

La mayoría de la gente dice que nunca lanza una excepción a un destructor, ya que esto provoca un comportamiento indefinido. Stroustrup señala que " el vector destructor invoca explícitamente el destructor para cada elemento. Esto implica que si un destructor de elementos lanza, 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 ninguna garantía si un destructor de elementos lanza " (del Apéndice E3.2) .

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

Así que 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 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.

¿Fue útil?

Solución

Lanzar una excepción desde 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 utilizando 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).

En efecto, usted pasa 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 del negocio.

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 quiere 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, el destructor tendrá que manejar la situación.

Scott Myers tiene un excelente artículo sobre el tema en su libro "Effective C ++"

Editar:

Aparentemente también en " Más eficaz C ++ "
Artículo 11: Prevenir las excepciones de los destructores

Otros consejos

La expulsión de un destructor puede provocar un bloqueo, 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 se insertaron en la pila desde que " intente " y hasta que se haya lanzado la excepción, se cancelará - > Se llamará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 abortar (), el programa 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 d'tors de objetos dentro de los contenedores. (Y se puede ignorar parcialmente, ya que algunos objetos no son adecuados para colocarlos 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:

  • (R) publica semántica (también conocido como free that memory)
  • (C) commit semántica (también conocido como flush en el disco)

Si vemos la pregunta de esta manera, entonces creo que se puede argumentar que (R) la semántica nunca debe causar una excepción de un dtor, ya que a) no hay nada que podamos hacer al respecto yb) muchos recursos libres. Las operaciones ni siquiera permiten 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 (" ámbito protegido ") conexión de base de datos que realiza una confirmación en el dtor son de un tipo diferente: podemos haga algo acerca del error (en el nivel de la 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 semántica (C) en sus d'tors, creo que también debemos 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 un ditor de confirmación se lanza mientras hay otra excepción activa.


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

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

(Debo tener en cuenta que otros también tenían ideas similares.)

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

En el future , puede ser una característica estándar para esto, ver N3614 , y un discusión al respecto .

Actualizaciones '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 guardia y registra el número de excepciones no capturadas   En su constructor. La salida es realizada por el objeto de guardia   destructor a menos que foo () lance ( en cuyo caso el número de no capturados   excepciones en el destructor es mayor que lo que el constructor   observado )

La verdadera pregunta que debes hacerte sobre lanzar desde un destructor es: "¿Qué puede hacer la persona que llama con esto?" ¿Hay 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 eso? Puedo registrarlo, o puedo ignorarlo. Eso es todo. No puedo " arreglar " es 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?

Es peligroso, pero tampoco tiene sentido desde el punto de vista de legibilidad / comprensión de 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 forma 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 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. El objeto 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 esta Preguntas frecuentes

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 objetos automáticos construidos en la ruta desde un bloque try a un throw-     la expresión se denomina & # 8220; retiro de pila. & # 8221; [Nota: Si un destructor llamado durante el desenrollado de la pila sale con un     excepción, std :: terminate se llama (15.5.1). Por lo tanto, los destructores generalmente deben capturar excepciones y no dejar que     Se propagan fuera del destructor. & # 8212; nota final]

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 varios objetos en un estado inconsistente, causando aún más problemas e 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 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...
};

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 es tan malo como el comportamiento de excepción, que, recordemos, es un comportamiento indefinido de su programa. Algunas de las objeciones del autor incluyen " estéticamente fea " y "fomenta el estilo malo". Ahora, ¿cuál preferirías tener? ¿Un programa con mal estilo o uno que exhibió un comportamiento indefinido?

Estoy en el grupo que considera que el " guardia de ámbito " el lanzamiento de patrones en 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 & # 324; ski tiene una excelente publicación 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 seguirá agregando invisiblemente la especificación noexcept a tu destructor. Y esto significa que en el momento en que su destructor lance una excepción, se llamará a 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) ,
  •   
  • Herede tu clase de otra que ya especifica 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 decides lanzar el destructor, siempre debes tener en cuenta el riesgo de una doble excepción (lanzar mientras la pila se está desenrollando debido a una excepción). Esto provocarí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 () .

  

P: Entonces mi pregunta es esta: si   arrojando desde un destructor resulta en   comportamiento indefinido, como manejas   ¿Errores que ocurren durante un destructor?

A: hay varias opciones:

  1. Deja que las excepciones salgan de tu destructor, independientemente de lo que esté sucediendo en otro lugar. Y al hacerlo, tenga en cuenta (o incluso tenga miedo) que puede seguir std :: terminate.

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

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

¿Pero es bueno lanzar 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 para lanzar desde un destructor. Al igual que el " debe comprobar " código de error. 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 .

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 público " cerrar " Método para realizar la operación que podría fallar ...

... pero creo que los destructores para clases de tipo contenedor, como un vector, no deben enmascarar las excepciones generadas por las clases que contienen. En este caso, en realidad uso un " free / close " Método que se llama a sí mismo recursivamente. Sí, dije recursivamente. Hay un método para esta locura. La propagación de excepciones se basa en que haya una pila: si se produce una sola excepción, los dos destructores restantes 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.

Martin Ba (arriba) está en el camino correcto: su arquitectura es diferente para la lógica RELEASE y COMMIT.

Para 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 envoltura RAII que cosas como std :: lock_guard proporcionan para mutexes. Con aquellos no pones la lógica de cometer en el dtor AT ALL. Tiene una API dedicada para ella, 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.

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

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á de inmediato.

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

  

Así que mi pregunta es esta: si el lanzamiento de un destructor resulta en   comportamiento indefinido, ¿cómo maneja los errores que ocurren durante un   destructor?

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

Dado que los destructores se invocan para las 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 asegurarse es que los cambios de esa estructura / archivo externo sean una transacción atómica que no puede fallar.

  

La solución pragmática es tal vez solo asegurarse de que las posibilidades de   El fracaso en el fracaso es astronómicamente improbable, ya que hacer cosas   imposible 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 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 que veo para hacerlo. A veces creo que debería haber una capacidad para escribir la lógica del destructor por separado para las rutas de ejecución normales a las de los 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 las rutas de destrucción excepcionales de las no excepcionales).

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 modelas tus diseños en este tipo de nivel más grueso, tienes que lidiar con menos destructores no triviales y, a menudo, también puedes pagar cualquier gasto de memoria / procesamiento necesario para asegurarte de que tus 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, podría hacerlo todo por el sistema de partículas 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. / p>

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 todos tienen dorsores no triviales.

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

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top