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.