Excepciones, destructores y técnica RAII (III)


Desenredo de la pila


La emisión de excepciones es el mecanismo natural de información en C++ de los errores recuperables acontecidos durante la ejecución de un programa. Una función que, por la razón que fuera, sea incapaz de completar satisfactoriamente su tarea puede informar del error al agente invocador emitiendo una excepción (típicamente, un objeto de una clase definida a tal efecto) a través de una expresión de tipo throw. El código que, explícita o implícitamente, realice la llamada a dicha función y deba hacerse cargo de los posibles errores, deberá encerrar el punto de invocación en un bloque try y capturar y manejar las excepciones emitidas a través de una o varias cláusulas catch. El esquema básico de actuación es, pues, el siguiente:

   struct Error { }; // clase de excepción    void g()    {       // ...       if (/* tiene lugar una situación de error */)          throw Error{};       // ...    }    void f()    {       try {          g();       }       catch (Error& e) {          // manejamos cualquier excepción de tipo Error emitida por g()       }    }

Al producirse una excepción dentro de un bloque try, el control del flujo se transfiere desde el punto de lanzamiento de la misma hasta la primera cláusula catch que pueda manejarla. Al alcanzarse dicha cláusula, todos los objetos con almacenamiento automático que hayan sido creados desde el inicio del bloque try hasta el punto de lanzamiento de la excepción son destruidos en orden inverso a su creación (invocándose a los destructores de sus clases de forma automática), en un proceso de desenredo de la pila [1].

Conviene tener en cuenta varias importantes puntualizaciones:
  1. Se recomienda que las excepciones sean siempre capturadas por referencia.
  2. El destructor de una clase no debe emitir excepciones bajo ninguna circunstancia. Si un destructor llamado durante un desenredo de la pila lanza una excepción, la función std::terminate [2] es invocada en tiempo de ejecución.
  3. La función std::terminate es también invocada de no encontrarse una cláusula catch capaz de manejar una excepción cualquiera emitida por el programa, en cuyo caso el que se produzca o no un desenredo de la pila es algo, según el estándar del lenguaje, dependiente de la implementación [3].
  4. Si se produce una excepción durante la construcción de un objeto que consta de subobjetos, se llamará a los destructores de los subobjetos que hayan sido creados satisfactoriamente antes de la emisión de la excepción, también en orden inverso a su creación. Al no alcanzarse el final del cuerpo del constructor, el objeto mencionado no llega a crearse, punto éste que analizaremos en más detalle a continuación.
De acuerdo con lo dicho, la ejecución del siguiente programa produce la lista de mensajes indicada más abajo:

   #include<iostream>    struct Error { }; // clase de excepción    struct X {       X() { std::cout << "X's constructor\n"; }       ~X() { std::cout << "X's destructor\n"; }    };    struct Y {       Y() { std::cout << "Y's constructor\n"; }       ~Y() { std::cout << "Y's destructor\n"; }    };    struct Z {       Y y; // subobjecto       Z() : y{}         // (D)       {          // emitimos una excepción de forma incondicional:          std::cout << "Z's constructor throws an Error type exception\n";          throw Error{}; // (E)       }       ~Z() { std::cout << "Z's destructor\n"; }    };    void f()    {       std::cout << "f() function call\n";       auto z = Z{}; // (C) el constructor de Z emite excepción       std::cout << "Exiting f() function\n"; // esta impresión no se ejecuta nunca    }    auto main() -> int    {       try {          auto x = X{};        // (A)          f();                 // (B)       }       catch (Error const&) {  // (F)          std::cerr << "Exception caught";       }    }    /* Output:       X's constructor       f() function call       Y's constructor       Z's constructor throws an Error type exception       Y's destructor       X's destructor       Exception caught */

En su ejecución, el programa crea primeramente el objeto x de tipo X (véase el punto A). Se llama entonces a la función f() (en B), en la cual se invoca al constructor de la clase Z con el fin de inicializar el objeto z (en C). Dentro del constructor, el subobjeto y de tipo Y llega a inicializarse correctamente antes de producirse la excepción (véanse D y E). Al emitirse la excepción, el control es transferido a la cláusula catch (en F) capacitada para manejar excepciones de tipo Error, proceso durante el cual el subobjeto y y el objeto x son destruidos (por ese orden) en el desenredo de la pila. Observemos que el destructor de la clase Z no es invocado en ningún momento, puesto que el objeto z nunca llega a existir. En efecto, el tiempo de vida de un objeto empieza al alcanzarse con éxito la llave de cierre (}) del constructor que lo crea. Dado que la excepción es emitida en nuestro caso por el constructor de la clase Z durante la inicialización del objeto z, su creación queda inconclusa.


Referencias bibliográficas:
  1. Stack unwinding - https://docs.microsoft.com/en-us/cpp/cpp/exceptions-and-stack-unwinding-in-cpp?view=vs-2019
  2. std::terminate - https://en.cppreference.com/w/cpp/error/terminate
  3. Implementation defined behavior - https://stackoverflow.com/questions/2397984/undefined-unspecified-and-implementation-defined-behavior

Puedes acceder al siguiente artículo de la serie a través de este enlace.

No hay comentarios:

Publicar un comentario