arrojando excepciones de un destructor

257

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.

Greg Rogers
fuente
36
"Dos excepciones a la vez" es una respuesta común, pero no es la verdadera razón. La verdadera razón es que se debe lanzar una excepción si y solo si las condiciones posteriores de una función no se pueden cumplir. La condición posterior de un destructor es que el objeto ya no existe. Esto no puede no suceder. Por lo tanto, cualquier operación de final de vida propensa a fallas debe llamarse como un método separado antes de que el objeto salga del alcance (las funciones sensibles normalmente solo tienen una ruta de éxito de todos modos).
rociar
29
@spraff: ¿Eres consciente de que lo que dijiste implica "tirar RAII"?
Kos
16
@spraff: tener que llamar "un método separado antes de que el objeto salga del alcance" (como usted escribió) en realidad arroja RAII! El código que utiliza dichos objetos tendrá que garantizar que se invocará dicho método antes de que se llame al destructor. Finalmente, esta idea no ayuda en absoluto.
Frunsi
8
@Frunsi no, porque este problema se debe al hecho de que el destructor está tratando de hacer algo más allá de la mera liberación de recursos. Es tentador decir "siempre quiero terminar haciendo XYZ" y pensar que este es un argumento para poner esa lógica en el destructor. No, no seas perezoso, escribe xyz()y mantén el destructor limpio de lógica que no sea RAII.
rociar el
66
@Frunsi Por ejemplo, confirmar algo para archivar no es necesariamente correcto en el destructor de una clase que representa una transacción. Si el commit falla, es demasiado tarde para manejarlo cuando todo el código que estuvo involucrado en la transacción se ha salido del alcance. El destructor debe descartar la transacción a menos commit()que se llame a un método.
Nicholas Wilson el

Respuestas:

198

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

#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 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

Martin York
fuente
55
"A menos que no le importe terminar potencialmente la aplicación, probablemente deba tragarse el error". - esta debería ser probablemente la excepción (perdón por el juego de palabras) en lugar de la regla, es decir, fallar rápido.
Erik Forbes
15
Estoy en desacuerdo. La finalización del programa detiene el despliegue de la pila. No se llamará más destructor. Cualquier recurso abierto se dejará abierto. Creo que tragar la excepción sería la opción preferida.
Martin York
20
El sistema operativo puede limpiar recursos de los que es propietario. Memoria, FileHandles, etc. ¿Qué pasa con los recursos complejos: conexiones de base de datos. Ese enlace ascendente a la ISS que abrió (¿enviará automáticamente las conexiones cercanas)? ¡Estoy seguro de que la NASA querría que cerraras la conexión limpiamente!
Martin York
77
Si una aplicación va a "fallar rápidamente" al abortar, no debería arrojar excepciones en primer lugar. Si va a fallar al pasar el control de vuelta a la pila, no debe hacerlo de una manera que pueda hacer que el programa sea abortado. Uno u otro, no elijas ambos.
Tom
2
@LokiAstari ¿El protocolo de transporte que está utilizando para comunicarse con una nave espacial no puede manejar una conexión caída? Ok ...
doug65536
54

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.

Gal Goldman
fuente
1
¿podría explicar cómo se llamó a abort () en la situación anterior. Significa que el control de ejecución todavía estaba con el compilador de C ++
Krishna Oza
1
@ Krishna_Oza: Muy simple: cada vez que se arroja un error, el código que genera un error verifica un bit que indica que el sistema de tiempo de ejecución está en proceso de desenrollado de la pila (es decir, manejando otro throwpero catchtodavía no ha encontrado un bloque) en cuyo caso se invoca std::terminate(no abort) en lugar de generar una (nueva) excepción (o continuar el desbobinado de la pila).
Marc van Leeuwen
53

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:

  • (R) liberar semántica (también conocido como liberar esa memoria)
  • (C) confirme la semántica (también conocido como archivo de vaciado al disco)

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 UncaughtExceptionCounterpara sus ScopeGuardherramientas.

(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 , no puede ser 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_exceptionsafaikt. Citaré rápidamente el artículo de cppref:

Notas

Un ejemplo donde se utiliza int-retorno uncaught_exceptionses ... ... primero crea un objeto protector y registra el número de excepciones no detectadas en su constructor. La salida es realizada por el destructor del objeto de protección a menos que arroje foo () ( en cuyo caso el número de excepciones no capturadas en el destructor es mayor de lo que observó el constructor )

Martin Ba
fuente
66
Muy de acuerdo. Y agregando una semántica de reversión semántica (Ro) más. Usado comúnmente en guarda de alcance Como el caso en mi proyecto donde definí una macro ON_SCOPE_EXIT. El caso sobre la semántica de reversión es que cualquier cosa significativa podría suceder aquí. Así que realmente no deberíamos ignorar el fracaso.
Weipeng L
Creo que la única razón por la que hemos cometido semántica en destructores es que C ++ no es compatible finally.
user541686
@Mehrdad: 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.
Martin Ba
1
@ MartinBa: Creo que te perdiste el punto de mi comentario, lo cual es sorprendente ya que estaba de acuerdo con tu idea de que (R) y (C) son diferentes. Estaba tratando de decir que un dtor es inherentemente una herramienta para (R) y finallyes inherentemente una herramienta para (C). Si no ve por qué: considere por qué es legítimo lanzar excepciones una encima de la otra en finallybloques, 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, finallyes para liberar el control Ellos son diferentes, es lamentable que en C ++ lazos juntos..)
user541686
1
@Mehrdad: Llegar demasiado tiempo aquí. Si lo desea, puede desarrollar sus argumentos aquí: programmers.stackexchange.com/questions/304067/… . Gracias.
Martin Ba
21

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 Fooobjeto y el Foodestructor arroja una excepción, ¿qué puedo hacer razonablemente con él? Puedo registrarlo o puedo ignorarlo. Eso es todo. No puedo "arreglarlo" porque el Fooobjeto 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?

Derek Park
fuente
11
Acabo de notar ... lanzar desde un dtor nunca es un Comportamiento Indefinido. Claro, podría llamar a terminar (), pero ese es un comportamiento muy bien especificado.
Martin Ba
44
std::ofstreamEl 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.
Andy
13

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.

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

¿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

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 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

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

};

Consulte también estas preguntas frecuentes

Doug T.
fuente
Resucitando esta respuesta, re: el primer ejemplo, acerca de 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.
tyree731
13

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.

3 El proceso de invocar destructores para objetos automáticos construidos en la ruta desde un bloque try a una expresión throw se llama "desbobinado de pila". [Nota: Si un destructor llamado durante el desbobinado de la pila sale con una excepción, se llama std :: terminate (15.5.1). Por lo tanto, los destructores generalmente deberían detectar excepciones y no dejar que se propaguen fuera del destructor. - nota final]

lothar
fuente
1
No respondió la pregunta, el OP ya lo sabe.
Arafangion
2
@Arafangion Dudo que él fuera consciente de esto (std :: terminar siendo llamado) ya que la respuesta aceptada hizo exactamente el mismo punto.
lothar
@Arafangion como en algunas respuestas aquí, algunas personas mencionaron que se llamaba a abort (); ¿O es que std :: terminate por turnos llama a la función abort ().
Krishna Oza
7

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.

Franci Penov
fuente
7

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::terminateya que los destructores están anotados implícitamente 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 noexceptpara 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 aún agregará invisiblemente especificación noexcepta su destructor. Y esto significa que en el momento en que su destructor arroje una excepción, std::terminatese llamará, incluso si no hubo una situación de doble excepción. Si realmente está decidido a permitir que sus destructores lancen, tendrá que especificar esto explícitamente; tienes tres opciones:

  • Especifique explícitamente su destructor como noexcept(false),
  • Herede su clase de otra que ya especifica su destructor como noexcept(false).
  • Coloque un miembro de datos no estático 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 (lanzar mientras la pila se desenrolla debido a una excepción). Esto provocaría una llamada std::terminatey rara vez es lo que desea. Para evitar este comportamiento, simplemente puede verificar si ya hay una excepción antes de lanzar una nueva usando std::uncaught_exception().

GaspardP
fuente
6

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:

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...
};
Tom
fuente
Estoy buscando una solución, pero están tratando de explicar qué sucedió y por qué. Solo quiero aclarar si se llama a la función de cierre dentro del destructor.
Jason Liu el
5

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?

DJClayworth
fuente
1
No es un comportamiento indefinido, sino una terminación inmediata.
Marc van Leeuwen
El estándar dice 'comportamiento indefinido'. Ese comportamiento es frecuentemente una terminación, pero no siempre es así.
DJClayworth
No, lea [excepto.terminado] en Manejo de excepciones-> Funciones especiales (que es 15.5.1 en mi copia del estándar, pero su numeración probablemente esté desactualizada).
Marc van Leeuwen
2

P: 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?

A: hay varias opciones:

  1. 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.

  2. Nunca permita que la excepción fluya de su destructor. Puede escribir en un registro, algún texto rojo grande y malo si puede.

  3. mi favorito : si std::uncaught_exceptiondevuelve 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 .

MartinP
fuente
44
Tu favorito es algo que probé recientemente, y resulta que no deberías hacerlo. gotw.ca/gotw/047.htm
GManNickG
1

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.

Mateo
fuente
1

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.

Devesh Agrawal
fuente
1
Escribir un mensaje en el archivo de registro puede causar una excepción.
Konard
1

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.

usuario3726672
fuente
1

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?

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 pragmática es quizás 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 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.

Dragon Energy
fuente
1
¿La destrucción es un fracaso ahora?
curioso
Creo que quiere decir que los destructores son llamados durante una falla, para limpiar esa falla. Entonces, si se llama a un destructor durante una excepción activa, entonces no se puede limpiar de una falla anterior.
user2445507
0

Establecer un evento de alarma. Por lo general, los eventos de alarma son una mejor forma de notificar fallas mientras se limpian objetos

MRN
fuente