c++ c++11 exception exception-handling nested-exceptions

¿Por qué C++ no usa std:: nested_exception para permitir el lanzamiento desde el destructor?



c++11 exception-handling (5)

El problema que cita ocurre cuando su destructor se está ejecutando como parte del proceso de desenrollado de la pila (cuando su objeto no se creó como parte del desenrollado de la pila) 1 , y su destructor necesita emitir una excepción.

Entonces, ¿cómo funciona eso? Tienes dos excepciones en juego. La excepción X es la que hace que la pila se desenrolle. La excepción Y es la que el destructor quiere lanzar. nested_exception solo puede contener uno de ellos.

Entonces, tal vez tenga una excepción Y contenga una excepción nested_exception (o tal vez solo una exception_ptr ). Entonces ... ¿cómo lidias con eso en el sitio de catch ?

Si atrapa Y , y resulta que tiene algo de X incrustado, ¿cómo lo obtiene? Recuerde: exception_ptr se borra por tipo ; aparte de pasarlo, lo único que puedes hacer es volver a tirarlo. Entonces la gente debería estar haciendo esto:

catch(Y &e) { if(e.has_nested()) { try { e.rethrow_nested(); } catch(X &e2) { } } }

No veo mucha gente haciendo eso. Especialmente porque habría un número extremadamente grande de posibles X -es.

1 : No utilice std::uncaught_exception() == true para detectar este caso. Es extremadamente defectuoso.

El principal problema al lanzar excepciones desde el destructor es que en el momento en que se llama al destructor, otra excepción puede estar "en vuelo" ( std::uncaught_exception() == true ) y, por lo tanto, no es obvio qué hacer en ese caso. "Sobrescribir" la antigua excepción con la nueva sería una de las formas posibles de manejar esta situación. Pero se decidió que std::terminate (u otro std::terminate_handler ) debe llamarse en tales casos.

C ++ 11 introdujo la función de excepciones anidadas a través de la clase std::nested_exception . Esta característica podría usarse para resolver el problema descrito anteriormente. La antigua excepción (no detectada) podría simplemente anidarse en la nueva excepción (¿o viceversa?) Y luego podría lanzarse esa excepción anidada. Pero esta idea no fue utilizada. std::terminate todavía se llama en tal situación en C ++ 11 y C ++ 14.

Entonces las preguntas. ¿Se consideró la idea con excepciones anidadas? ¿Hay algún problema con eso? ¿No se va a cambiar la situación en C ++ 17?


El problema que puede ocurrir durante el desbobinado de la pila con el encadenamiento de excepciones de los destructores es que la cadena de excepciones anidadas puede ser demasiado larga. Por ejemplo, tiene std::vector de 1 000 000 elementos, cada uno de los cuales arroja una excepción en su destructor. Supongamos que el destructor de std::vector recopila todas las excepciones de los destructores de sus elementos en una sola cadena de excepciones anidadas. Entonces la excepción resultante puede ser incluso mayor que el contenedor std::vector original. Esto puede causar problemas de rendimiento e incluso arrojar std::bad_alloc durante el desbobinado de la pila (que incluso no pudo anidarse porque no hay suficiente memoria para hacerlo) o arrojar std::bad_alloc en otros lugares no relacionados en el programa.


El verdadero problema es que tirar de los destructores es una falacia lógica. Es como definir el operador + () para realizar la multiplicación. Los destructores no deben usarse como ganchos para ejecutar código arbitrario. Su propósito es liberar recursos de manera determinista. Por definición, eso no debe fallar. Cualquier otra cosa rompe los supuestos necesarios para escribir código genérico.


Hay un uso para std::nested exception , y solo un uso (por lo que he podido descubrir).

Dicho esto, es fantástico, utilizo excepciones anidadas en todos mis programas y, como resultado, el tiempo dedicado a buscar errores oscuros es casi cero.

Esto se debe a que las excepciones de anidación le permiten crear fácilmente una pila de llamadas totalmente anotada que se genera en el punto del error, sin sobrecarga de tiempo de ejecución, sin necesidad de un registro abundante durante una nueva ejecución (lo que cambiará el tiempo de todos modos), y sin contaminar la lógica del programa con manejo de errores.

por ejemplo:

#include <iostream> #include <exception> #include <stdexcept> #include <sstream> #include <string> // this function will re-throw the current exception, nested inside a // new one. If the std::current_exception is derived from logic_error, // this function will throw a logic_error. Otherwise it will throw a // runtime_error // The message of the exception will be composed of the arguments // context and the variadic arguments args... which may be empty. // The current exception will be nested inside the new one // @pre context and args... must support ostream operator << template<class Context, class...Args> void rethrow(Context&& context, Args&&... args) { // build an error message std::ostringstream ss; ss << context; auto sep = " : "; using expand = int[]; void (expand{ 0, ((ss << sep << args), sep = ", ", 0)... }); // figure out what kind of exception is active try { std::rethrow_exception(std::current_exception()); } catch(const std::invalid_argument& e) { std::throw_with_nested(std::invalid_argument(ss.str())); } catch(const std::logic_error& e) { std::throw_with_nested(std::logic_error(ss.str())); } // etc - default to a runtime_error catch(...) { std::throw_with_nested(std::runtime_error(ss.str())); } } // unwrap nested exceptions, printing each nested exception to // std::cerr void print_exception (const std::exception& e, std::size_t depth = 0) { std::cerr << "exception: " << std::string(depth, '' '') << e.what() << ''/n''; try { std::rethrow_if_nested(e); } catch (const std::exception& nested) { print_exception(nested, depth + 1); } } void really_inner(std::size_t s) try // function try block { if (s > 6) { throw std::invalid_argument("too long"); } } catch(...) { rethrow(__func__); // rethrow the current exception nested inside a diagnostic } void inner(const std::string& s) try { really_inner(s.size()); } catch(...) { rethrow(__func__, s); // rethrow the current exception nested inside a diagnostic } void outer(const std::string& s) try { auto cpy = s; cpy.append(s.begin(), s.end()); inner(cpy); } catch(...) { rethrow(__func__, s); // rethrow the current exception nested inside a diagnostic } int main() { try { // program... outer("xyz"); outer("abcd"); } catch(std::exception& e) { // ... why did my program fail really? print_exception(e); } return 0; }

Rendimiento esperado:

exception: outer : abcd exception: inner : abcdabcd exception: really_inner exception: too long

Explicación de la línea de expansión para @Xenial:

void (expand{ 0, ((ss << sep << args), sep = ", ", 0)... });

args es un paquete de parámetros. Representa 0 o más argumentos (el cero es importante).

Lo que estamos buscando hacer es hacer que el compilador expanda el paquete de argumentos para nosotros mientras escribe código útil a su alrededor.

Tomémoslo desde afuera en:

void(...) - significa evaluar algo y tirar el resultado - pero evaluarlo.

expand{ ... };

Recordando que expand es un typedef para int [], esto significa que vamos a evaluar una matriz entera.

0, (...)...;

significa que el primer entero es cero; recuerde que en c ++ es ilegal definir una matriz de longitud cero. ¿Qué pasa si args ... representa 0 parámetros? Este 0 asegura que la matriz tenga al menos un número entero.

(ss << sep << args), sep = ", ", 0);

usa el operador de coma para evaluar una secuencia de expresiones en orden, tomando el resultado de la última. Las expresiones son:

s << sep << args : imprime el separador seguido del argumento actual en la secuencia

sep = ", " - luego haz que el separador apunte a una coma + espacio

0 - da como resultado el valor 0. Este es el valor que va en la matriz.

(xxx params yyy)... - significa hacer esto una vez para cada parámetro en los parámetros del paquete de parámetros

Por lo tanto:

void (expand{ 0, ((ss << sep << args), sep = ", ", 0)... });

significa "para cada parámetro en los parámetros, imprímalo en ss después de imprimir el separador. Luego actualice el separador (para que tengamos un separador diferente para el primero). Haga todo esto como parte de la inicialización de una matriz imaginaria que luego arrojaremos lejos.


Las excepciones anidadas simplemente agregan la información más probable ignorada sobre lo que sucedió, que es esto:

Se ha lanzado una excepción X, la pila se está desenrollando, es decir, los destructores de objetos locales se están llamando con esa excepción "en vuelo", y el destructor de uno de esos objetos a su vez lanza una excepción Y.

Por lo general, esto significa que la limpieza falló.

Y luego, esto no es una falla que se pueda remediar al informarlo hacia arriba y dejar que el código de nivel superior decida, por ejemplo, usar algunos medios alternativos para lograr su objetivo, porque el objeto que contenía la información necesaria para hacer la limpieza ha sido destruido , junto con con su información, pero sin hacer su limpieza. Entonces es muy parecido a una afirmación que falla. El estado del proceso puede ser muy malo, rompiendo los supuestos del código.

Los destructores que arrojan pueden en principio ser útiles, por ejemplo, como la idea que Andrei emitió una vez sobre indicar una transacción fallida a la salida de un alcance de bloque. Es decir, en la ejecución normal de código, un objeto local que no ha sido informado del éxito de la transacción puede arrojarse desde su destructor. Esto solo se convierte en un problema cuando choca con la regla de excepción de C ++ durante el desenrollado de la pila, donde requiere la detección de si se puede lanzar la excepción, lo que parece imposible. De todos modos, el destructor se está utilizando solo para su llamada automática, no en su rol de limpieza. Y así se puede concluir que las reglas actuales de C ++ asumen el rol de limpieza para los destructores.