Manejo de Excepciones en C++

Lisardo Fernández Cordeiro
1º GII  –  ETSE
Universidad de Valencia



A poco que uno se ilustra convenientemente, intentando no confundirse con lo que le conviene, asume como cierto lo que ya Marco Tulio Cicerón expresaba ricamente pocos años antes del reset en el calendario: “de humanos es equivocarse”. Y si esto es así, el fruto del trabajo del hombre, como es el software, necesariamente está abocado a un mar de errores y situaciones anómalas no contempladas. Afortunadamente, Kung FuTse –Confucio para los amigos- ya había previsto bálsamo para este alumbramiento unos 500 años antes: “los cautelosos muy poco se equivocan”. Lo que conduce a tejer estrategias convenientemente medidas que establezcan estructuras de control adecuadas para lidiar con lo que se ha dado en llamar excepciones.

códigos de error
Hasta la fecha nos hemos enfrentado a las excepciones –vistas en su término amplio de error o situación anómala- a partir de la reconversión de funciones o procedimientos void obligando a la devolución de un código de error que informe del éxito o fracaso de la operación solicitada. Pero, además de enmarañar excesivamente el código, no siempre se pueden utilizar. Piénsese en una excepción dentro de un constructor, que no puede devolver código.
Una opción a las limitaciones descritas pasa por la construcción de objetos con un dato de control interno responsable de informar de excepciones ocurridas en su deambular a lo largo del código. Algo así como un no saberse sano o enfermo, obligando a todos los métodos, procedimientos o funciones donde opere, a la verificación previa de su estado de salud. Sinceramente, esta solución asoma a códigos gigantescos de incierto mantenimiento.
Con lo anterior, se puede deducir que el manejo de excepciones no es sencillo, pero de alguna manera se debe paliar el daño que pueden llegar a provocar en los sistemas. Veamos como.

conceptos básicos
Para fortuna de los programadores y por ende, de los usuarios de sistemas sensibles o de “misión crítica” –léase centrales nucleares, control de presas, gestión de tráfico, fábricas de nuestra gama de gominolas favorita- C++ incorpora tres construcciones explicitadas en tres palabras reservadas a saber:
            … la expresión throw responsable de lanzar las excepciones
            … el bloque try que alberga la porción de código estimada como sensible.
            … el bloque catch donde se definen los manejadores de las excepciones.
El funcionamiento básico del conjunto de herramientas descrito es, en primera aproximación, transparente al código, de hecho, el compilador lo trata de manera diferenciada al situarlo en un nivel superior, actuando las excepciones como los humanos en sus plegarias ante lo inevitable: encomendados a los designios de un ser superior. En esencia, throw puede situarse en cualquier parte del código tal que así:

throw 5; // lanza el objeto 5 (de tipo int)
throw Persona(); // lanza un objeto por defecto de tipo Persona
throw Racional( 1,2 ); // lanza el objeto Racional ½
throw “Error de suma”; // lanza un objeto de tipo char*
Sin embargo, el bloque catch, con argumentos o no,  está inmisericordemente ligado al bloque try que no precisa de argumentos, situándose a continuación del mismo. Algo así como la estructura del condicional IF con su ELSE, salvo que pueden describirse diferentes manejadores catch.
Veamos un ejemplo:
try {
// una porción de código cualquiera incluyendo los throw
}
catch ( int entero ) { // inmediatamente después del bloque try
cout << “Código de error: ” << entero;
}
catch ( Persona ) {
// en este “catch” sólo importa el tipo de error
// y no el objeto concreto lanzado por “throw”
relanzaProcesoDePersona();
}
catch ( Racional& racional ) {
racional += 1;
cout << racional;
}
catch ( const char* mensaje ) {
VentanaDeMensaje( mensaje );
}
Esencialmente, manejar las excepciones en C++ es un mecanismo de puesta en comunicación entre dos partes distintas de una aplicación. Podría parecer irrespetuoso con la concepción de la programación estructurada, si bien podemos salvar el pellejo asociando el modelo al de llamada a funciones, estableciendo las salvedades oportunas, puesto que las llamadas a funciones se realizan en tiempo de compilación y las llamadas a excepciones en tiempo de ejecución. Es tan así, que el compilador se centra en la generación de una estructura que albergue un descriptor de tipo para cada expresión throw y catch que permite su comparación en tiempo de ejecución.
Dentro del boque de código amparado por try, se lanzarán excepciones mediante la utilización de controladores del tipo:
if (condicion) throw «overflow»;
En ocasiones se utilizan como órdenes expresas cuando la misión sea el control del flujo de ejecución multinivel:
            throw “cielos, me he perdido”;
Y en otras serán las propias librerías de excepciones incorporadas por C++ las que lanzarán las excepciones previstas en ellas.
Todas serán tratadas por los manejadores previstos para ellas o por el comodín: catch (…). Por lo tanto, que nadie espere el tratamiento de una excepción no lanzada específicamente dentro de un bloque try, ni contemplada en el handler –que es como se denomina en el idioma del imperio al manejador-. El código, por mucho cariño que se le ponga, no es capaz de anticipar sus errores y mucho menos de resolverlos sin las herramientas que aquí se están exponiendo. Ya nos gustaría a los torpes aprendices de sabio.
Debemos ser cuidadosos en la estructuración de los sistemas de control de excepciones puesto que una vez lanzada, el mensajero throw corre raudo y veloz en busca del manejador catch que identifique el tipo y lo acepte para ejecutar el boque de código correspondiente. Pero el mensajero ni el manejador saben, al final de su proceso, de donde provenía el mensaje, con lo que continúa la ejecución del programa a renglón seguido del bloque catch, o su conjunto.  
El lector avispado pensará que existe una solución elegante estrechando los bloques de control suficientemente, con objeto de acercar el salto de la excepción y su tratamiento, paliando los problemas en la secuencia  de ejecución. Yo, desde luego no voy a ser quien le quite la ilusión, pero creo ha llegado el momento de reflexionar un poco: ¿qué sucede si la excepción ha saltado en el interior de una función llamada por otra con su propia gestión de excepciones, que a su vez ha sido llamada por la porción de código encerrada en un bloque try del main?. En este caso, sabemos que el funcionamiento va a ser semejante a la herencia: ir buscando el manejador adecuado desde el nivel de salto hacia arriba, perdiendo igualmente la secuencia normal de ejecución.
Pero, ¿qué sucede si se producen excepciones durante el manejo de las excepciones? Mmmm… querido amigo, no existe el sistema perfecto ni invulnerable. Se debe ser muy cuidadoso a la hora de programar tal y como recomienda el gurú danés Bjarne Stroustrup –papá de C++-:
“Aunque las excepciones se pueden usar para sistematizar el manejo de errores, cuando se adopta este esquema, debe prestarse atención para que cuando se lance una excepción no cause más problemas de los que pretende resolver. Es decir, se debe prestar atención a la seguridad de las excepciones. Curiosamente, las consideraciones sobre seguridad del mecanismo de excepciones conducen frecuentemente a un código más simple y manejable.”
Efectivamente, la panacea no está en esta pequeña introducción a las excepciones, sino en la adecuada implementación de ellas con el resto de recursos e instrucciones que ofrece C++.

clases de excepciones
Hasta ahora hemos podido observar el lanzamiento de excepciones con objetos de tipo predefinido. Sin embargo, también se pueden lanzar excepciones de clases definidas por el usuario.
Como estrategia para el manejo de las excepciones, se debe crear una serie de clases para tratar cada error. Por supuesto, si de clases estamos hablando, podríamos utilizar el mecanismo de herencia para construir una jerarquía de clases.
Para ello, la clase base deberá contener una mínima funcionalidad y un destructor. Observemos el siguiente ejemplo:

class Error {
/* clase base */
};
class ErrorBaseDeDatos : public Error {
/*…*/
};

class ErrorDeActualizacion : public ErrorBaseDeDatos {
/*…*/
};

void actualizaClientes() {
throw ErrorDeActualizacion();
}

try {
actualizaClientes();
}
catch ( ErrorDeActualizacion ) {
/*…*/
}
catch ( Error ) {
/*…*/
}
catch ( ErrorBaseDeDatos ) {
/*…*/
}
Observamos cómo la excepción lanzada en la función saltará al bloque try de donde procede su invocación, pasando a comprobar los manejadores asociados. Por supuesto, la comprobación es en riguroso orden de aparición, de modo que será atendida por el primer bloque que se encuentra con el que coincide el tipo. Pero, ¿qué hubiera pasado si se lanza una excepción de la clase ErrorBaseDeDatos?. El bloque que lo atendería es el correspondiente a la clase base de la que deriva, con lo que el último catch sería inalcanzable. Cuidado con esto, si se manejan tipos de datos en jerarquía, los handler con argumento de clases derivadas, deben mantenerse en posiciones anteriores a los que manejan excepciones de las clases base.

desapilando
En el proceso natural de ejecución de un programa, es de todos conocido –un lector siempre agradece estos detalles- que el sistema utiliza un stack para ir apilando y desapilando el nivel en el que se encuentra, construyendo al entrar y destruyendo al salir todo lo que es local, como variables, punteros, objetos y demás útiles para desenvolverse adecuadamente.
Sin embargo, al realizar un salto de nivel fruto del lanzamiento de una excepción, podríamos pensar que, al dejar abruptamente el nivel de procesamiento, se quedarán objetos abandonados, ocupando espacio de memoria inaccesible. Para evitar esto, los destructores de los objetos automáticos creados hasta que se encuentra el bloque try se invocan en el momento del lanzamiento.
Pese a lo bien que suena, es posible que objetos persistentes creados entre el inicio del bloque try y el lanzamiento de la excepción no se puedan destruir, lo que conlleva la adopción de una serie de precauciones especiales que no podremos ver en este breve trabajo introductorio.


ah! pero si hay más
Si pese a todo el cuidado del mundo mundial que se ha puesto en la estructuración e implementación de un código, resulta que salta una excepción que no encuentra manejador, se genera una excepción en la destrucción de un objeto o se produce algún error interno, nos hemos quedado desamparados y a dos velas.
Para estos casos C++ provee la función void terminate() la cual llama a la siempre amiga función abort() a través de su manejador. Esta situación no siempre es deseable, por lo que podríamos detener su feo impacto aprovechando la posibilidad de negar el lanzamiento a una particularidad que posee throw, relanzar la última excepción:
            terminate_handler set_terminate( terminate_handler f ) throw();
También puede suceder que una función con especificación de excepciones asociada, lance una excepción no contemplada en su lista. Esta situación produce una llamada a la función void unexpected()  la cual llamará por defecto a terminate() y otra vez la hemos liado.
Pero seguro que más de uno ha intuido que se podría utilizar el mismo artificio anterior, pues sí:
            unexpected_handler set_unexpected( unexpected_handler f ) throw();
Visto esto, si una vez salta unexpected() se lanza una excepción contemplada en la lista, todo sigue su camino normal, pero si la excepción no está en la lista, pasaríamos al siguiente nivel, es decir, se llamaría a terminate() y vuelta a empezar.
Para evitar esta situación se recurre a la inclusión del tipo bad_exception en la especificación de la función, tal y como se puede observar a continuación:
            void funcion() throw (int, Overflow, bad_exception );
lo que hará que continúe la búsqueda de un nuevo catcher en el ámbito de la llamada a la función con la especificación referida.

conclusión
Siempre, las palabras de los sabios han servido de inspiración y guía en los albores de cualquier actividad, así que nada mejor para concluir que las de Cay S. Hortsmann:
“Las excepciones deberán reservarse para circunstancias inesperadas en el flujo normal de una computación y cuya ocurrencia crea una situación que no puede ser resuelta en su actual ámbito”.

bibliografía

.. C++ STL, Plantillas, Excepciones, Roles y Objetos. –  Ricardo Devis Botella
.. Tratamiento de excepciones. http://www.zator.com/Cpp/E1_6.htm
.. Excepciones y su manejo. Javier Abreu Afonso
.. Excepciones en C++. David Martínez Serrano

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *