Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


1.6  Tratamiento de excepciones

'La respuesta real es hacer copias de seguridad de todo, con mucho cuidado y muy frecuentemente, poniéndose en lo peor, ya que la definición informática de "lo peor" es "solo cuestión de tiempo"'.  Paul Somerson "PC Magazine:  DOS Powers Tools".

§1  Introducción

El problema de la seguridad es uno de los clásicos quebraderos de cabeza de la programación. Los diversos lenguajes han tenido siempre que lidiar con el mismo problema: ¿Qué hacer cuando se presenta una circunstancia verdaderamente imprevista? (por ejemplo un error). El asunto es especialmente importante si se trata de lenguajes para escribir programas de "Misión crítica"; digamos por ejemplo controlar los ordenadores de una central nuclear o de un sistema de control de tráfico aéreo.

Antes que nada, digamos que en el lenguaje de los programadores C++ estas "circunstancias imprevistas" reciben el nombre de excepciones, por lo que el sistema que implementa C++ para resolver estos problemas recibe el nombre de manejador de excepciones.  Así pues, las excepciones son condiciones excepcionales que pueden ocurrir dentro del programa durante su ejecución. Por ejemplo, que ocurra una división por cero, se agote la memoria disponible, Etc. que requieren recursos especiales para su control.

En este capítulo trataremos del manejador de excepciones C++; una serie de técnicas que permiten formas normalizadas de manejar los errores, intentando anticiparse a los problemas potenciales previstos e imprevistos. Así como permitir al programador reconocerlos, fijar su ubicación y corregirlos.

§2  Manejo de excepciones en C++

El manejo de excepciones C++ se basa en un mecanismo cuyo funcionamiento tiene tres etapas básicas:

1:    Se intenta ejecutar un bloque de código y se decide qué hacer si se produce una circunstancia excepcional durante su ejecución.

2:    Se produce la circunstancia: se "lanza" una excepción (en caso contrario el programa sigue su curso normal).

3:    La ejecución del programa es desviada a un sitio específico donde la excepción es "capturada" y se decide que hacer al respecto.


¿Pero que es eso de "lanzar" y "capturar" una excepción"?   En general la frase se usa con un doble sentido:  Por un lado es un mecanismo de salto que transfiere la ejecución desde un punto (que "lanza" la excepción) a otro dispuesto de antemano para tal fin (que "captura" la excepción). A este último se le denomina manejador o "handler" de la excepción. Además del salto -como un goto-, en el punto de lanzamiento de la excepción se crea un objeto, a modo de mensajero, que es capturado por el "handler" (como una función que recibe un argumento). El objeto puede ser cualquiera, pero lo normal es que pertenezca a una clase especial definida al efecto, que contiene la información necesaria para que el receptor sepa qué ha pasado;  cual es la naturaleza de la circunstancia excepcional que ha "lanzado" la excepción [6].

Para las tres etapas anteriores existen tres palabras clave específicas: trythrow y catch. El detalle del proceso es como sigue.

§2.1  Intento  ( try ).

En síntesis podemos decir que el programa se prepara para cierta acción, decimos que "lo intenta". Para ello se especifica un bloque de código cuya ejecución se va a intentar ("try-block") utilizando la palabra clave try.

try {    // bloque de código-intento

  ...

}

El juego consiste en indicar al programa que si existe un error durante el "intento", entonces debe lanzar una excepción y transferir el control de ejecución al punto donde exista un manejador de excepciones ("handler") que coincida con el tipo lanzado. Si no se produce ninguna excepción, el programa sigue su curso normal.

De lo dicho se deduce inmediatamente que se pueden lanzar excepciones de varios tipos y que pueden existir también receptores (manejadores) de varios tipos; incluso manejadores "universales", capaces de hacerse cargo de cualquier tipo de excepción. A la inversa, puede ocurrir que se lance una excepción para la que no existe manejador adecuado, en cuyo caso... (la solución más adelante).

Así pues, try es una sentencia que en cierta forma es capaz de especificar el flujo de ejecución del programa. Un bloque-intento debe ser seguido inmediatamente por el bloque manejador de la excepción.

§2.2  Se lanza una excepción ( throw ).

Si se detecta una circunstancia excepcional dentro del bloque-intento,  se lanza una excepción mediante la ejecución de una sentencia throw. Por ejemplo:

if (condicion) throw "overflow";

Es importante advertir que, salvo los casos en que la excepción es lanzada por las propias librerías C++ (como consecuencia de un error 1.6.1a), estas no se lanzan espontáneamente.  Es el programador el que debe utilizar una sentencia (generalmente condicional) para, en su caso, lanzar la excepción.

El lenguaje C++ especifica que todas las excepciones deben ser lanzadas desde el interior de un bloque-intento y permite que sean de cualquier tipo. Como se ha apuntado antes, generalmente son un objeto (instancia de una clase) que contiene información.  Este objeto es creado y lanzado en el punto de la sentencia throw y capturado donde está la sentencia catch.  El tipo de información contenido en el objeto es justamente el que nos gustaría tener para saber que tipo de error se ha producido. En este sentido puede pensarse en las excepciones como en una especie de correos que transportan información desde el punto del error hasta el sitio donde esta información puede ser analizada.

§2.3  La excepción es capturada en un punto específico del programa ( catch ).

Esta parte del programa se denomina manejador ("handler");  se dice que el "handler" captura la excepción. El handler es un bloque de código diseñado para manejar la excepción precedido por la palabra catch. El lenguaje C++ requiere que exista al menos un manejador inmediatamente después de un bloque try. Es decir, se requiere el siguiente esquema:

try {         // bloque de código que se intenta

  ...

}

catch (...) { // bloque manejador de posibles excepciones

  ...

}

...           // continua la ejecución normal


El "handler" es el sitio donde continua el programa en caso de que ocurra la circunstancia excepcional (generalmente un error) y donde se decide qué hacer. A este respecto, las estrategias pueden ser muy variadas (no es lo mismo el programa de control de un reactor nuclear que un humilde programa de contabilidad). En último extremo, en caso de errores absolutamente irrecuperables, la opción adoptada suele consistir en mostrar un mensaje explicando el error. Puede incluir el consabido "Avise al proveedor del programa" o bien generar un fichero texto (por ejemplo: error.txt) con la información pertinente, que se guarda en disco con objeto de que pueda ser posteriormente analizado y corregido en sucesivas versiones de la aplicación [2].

Llegados a este punto debemos recordar que, como veremos en los ejemplos, las excepciones generadas pueden ser de diverso tipo (según el tipo de error), y que también pueden existir diversos manejadores. De hecho se debe incluir el manejador correspondiente a cada excepción que se pueda generar.

§3  Resumen

Hemos dicho que try es una sentencia que en cierta forma es capaz de especificar el flujo de ejecución del programa; en el fondo el mecanismo de excepciones de C++ funciona como una especie de sentencia if ... then .... else,  que tendría la forma:

If { este bloque se ejecuta correctamente }

then

seguir la ejecución normal de programa

else   // tres acciones sucesivas.

a. Crear un objeto con información del suceso (excepción)

b. Transferir el control de ejecución al "handler" correspondiente

c. Recibir el objeto para su análisis y decisión de la acción a seguir

en este caso la sintaxis utilizada es la siguiente:

try {         // bloque de código que se intenta

  ...

}

catch (...) { // captura de excepciones

  ...

}

...           // continua la ejecución normal


El diseño del mecanismo de excepciones C++, someramente expuesto, tiene la ventaja de permitir resolver una situación muy frecuente: el bloque en que se detecta el error no sabe que hacer en tal caso (cuando se presenta el error o excepción); la acción depende en realidad de un nivel anterior, el módulo que invocó la operación. Como decimos, esta situación es muy frecuente (), entre otras razones porque si un módulo pudiera anticipar un error por si mismo, también podría evitarlo, con lo que no habría necesidad de mecanismo de excepciones. Esta circunstancia es especialmente patente en el caso de librerías, en las que el autor generalmente no sabe ni puede hacer nada al respecto de ciertos errores a excepción de informar al usuario.

Lo anterior no es óbice para que, como buena práctica de programación, se intente la captura sistemática de errores lo más próximo posible a su identificación, dejando el mecanismo de excepciones para las situaciones realmente imprevisibles. Por ejemplo, siempre que sea posible:

if (b == 0) { /* alguna acción... */; }

else x = a/b;

§4  Precauciones

Cuando se plantean este tipos de cuestiones de seguridad surge inevitablemente una pregunta: ¿Que sucede si se producen errores (circunstancias excepcionales) durante el proceso de control de excepciones?.

La respuesta más honesta es que el sistema perfecto e invulnerable no existe. Aunque el sistema de excepciones de C++ es una formidable herramienta para controlar imprevistos, que permite hasta cierto punto, controlar imprevistos dentro de imprevistos. A pesar de ello, nada puede sustituir a una programación cuidadosa. El propio Stroustrup advierte: "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".

  Comentarios sobre la idoneidad del mecanismo de excepciones ( 1.6W2).

§5  Tipos de excepciones

Durante la ejecución de un programa pueden existir dos tipos de circunstancias excepcionales: síncronas y asíncronas. Las primeras son las que ocurren dentro del programa. Por ejemplo, que se agote la memoria o cualquier otro tipo de error. Son a estas a las que nos hemos estado refiriendo. En la introducción (§1 ) hemos indicado: "las excepciones son condiciones excepcionales que pueden ocurrir dentro del programa..." y las únicas que se consideran. Las excepciones asíncronas son las que tienen su origen fuera del programa, a nivel del Sistema Operativo. Por ejemplo que se pulsen las teclas Ctrl+C.

Generalmente las implementaciones C++ solo consideran las excepciones síncronas, de forma que no se pueden capturar con ellas excepciones tales como la pulsación de una tecla. Dicho con otras palabras:  solo pueden manejar las excepciones lanzadas con la sentencia throw.  Siguen un modelo denominado de excepciones síncronas con terminación, lo que significa que una vez que se ha lanzado una excepción, el control no puede volver al punto que la lanzó.

Nota:  El "handler" no puede devolver el control al punto de origen del error mediante una sentencia return.  En este contexto, un return en el bloque catch supone salir de la función que contiene dicho bloque.


El sistema Estándar C++ de manejo de excepciones no está diseñado para manejar directamente excepciones asíncronas, como las interrupciones de teclado, aunque pueden implementarse medidas para su control. Además las implementaciones más usuales disponen de recursos para menejar las excepciones del Sistema Operativo. Por ejemplo, C++ Builder dispone de los mecanismos adecuados para manejar excepciones de Windows-32 (asíncronas) a través de su librería VCL [1] ( 1.6w1).

§6  Secuencia de ejecución

Como puede verse, la filosofía C++ respecto al manejo de excepciones no consiste en corregir el error y volver al punto de partida. Por el contrario, cuando se genera una excepción el control sale del bloque-intento try que lanzó la excepción (incluso de la función), y pasa al bloque catch cuyo manejador corresponde con la excepción lanzada (si es que existe).

A su vez el bloque catch puede hacer varias cosas:

  • Relanzar la misma excepción ( 1.6.1).

  • Saltar a una etiqueta ( 1.6.2)

  • Terminar su ejecución normalmente (alcanzar la llave } de cierre).

Si el bloque-catch termina normalmente sin lanzar una nueva excepción, el control se salta todos los bloques-catch que hubiese a continuación y sigue después del último.

Puede ocurrir que el bloque-catch lance a su vez una excepción. Lo que nos conduce a excepciones anidadas. Esto puede ocurrir, por ejemplo, cuando en el proceso de limpieza de pila ("Stack unwinding") que tienen lugar tras una excepción, un destructor lanza una excepción .

  Como se verá en los ejemplos, además del manejo y control de errores, las excepciones C++ pueden utilizarse como un mecanismo de return o break multinivel, controlado no por una circunstancia excepcional, sino como un acto deliberado del programador para controlar el flujo de ejecución [5]. Ejemplos:  ( 4.5.8).

§7  Constructores y destructores en el manejo de excepciones

Cuando en el lanzamiento de excepciones se utilizan objetos por valor, throw llama al constructor copia (4.11.2d4).  Este constructor inicializa un objeto temporal en el punto de lanzamiento.

Si ocurren errores durante la construcción de un objeto, los constructores pueden lanzar excepciones [3]. Si un constructor lanza una excepción,  el destructor del objeto no es llamado necesariamente.

Cada vez que desde dentro de un bloque-try se lanza una excepción y el control sale fuera de bloque, tiene lugar un proceso de búsqueda y desmontaje descendente en la pila hasta encontrar el manejador ("Catcher") correspondiente. Durante este proceso, denominado "Stack unwinding", todos los objetos de duración automática que se crearon hasta el momento de ocurrir la excepción, son destruidos de forma controlada mediante llamadas a sus destructores. Si uno de los destructores invocados provoca a su vez una excepción que no tiene un "handler" adecuado, se produce una llamada a la función terminate ( 1.6.3).

Nota:  Observe que no se menciona para nada la destrucción de objetos persistentes que se hubiesen creado entre el inicio del bloque try y el punto de lanzamiento de la excepción. Esto origina una difícil convivencia entre el mecanismo de excepciones y el operador new. Para resolver los problemas potenciales deben adoptarse precauciones especiales ( 4.9.20).


La invocación a los destructores de los objetos automáticos, se realiza solo para aquellos objetos que hubiesen sido construidos totalmente a partir de la entrada en el bloque-intento (objetos cuyos constructores hubiesen finalizado satisfactoriamente). Si los objetos tienen sub-objetos, la invocación solo se realiza para los destructores de la clase-base.

Tema relacionado:  Control de recursos ( 4.1.5a)

Nota:  En el caso del C++Builder, los destructores son llamados por defecto, pero puede evitarse mediante la opción -xd- del compilador como se indica a continuación. Esta opción, como otras de este tipo, está gobernada por el valor de una constante global ( 1.4.1a).

§8  Establecer opciones de manejo de excepciones

Los compiladores suelen disponer de comandos de compilación ( 1.4.3) para determinar el tratamiento que seguirá el manejo de excepciones. A título de ejemplo se muestran algunos:

Opción    Compilador Descripción 
-x- Borland C++ Deshabilitar el manejo de excepciones C++  (habilitado por defecto).

Nota: la eliminación del mecanismo de excepciones en nuestro código no impide que estas puedan ser utilizadas en aquellas funciones de librería que lo tienen habilitado y sean enlazadas con nuestro ejecutable.  Por ejemplo, las rutinas de tiempo real de Borland.

-xd Borland C++ Habilita limpieza total. Llamada a los destructores para todos los objetos declarados automáticos (locales) entre el ámbito del capturador (catch) y el lanzador (throw) de la excepción cuando es lanzada la excepción  (activo por defecto). Si se activa esta opción, también hay que activar la opción –RT ( 4.9.14).
-xp Borland C++ Habilita información sobre las excepciones. Posibilita identificación de excepciones en tiempo de ejecución mediante la inclusión en el objeto del número de líneas del código fuente. Esto permite al programa interrogar el fichero y número de línea donde ha ocurrido una excepción utilizando los identificadores globales __ThrowFileName__ThrowLineNumber.
-fno-exceptions GNU c++ Deshabilitar el mecanismo de excepciones.  

Nota: los compiladores gcc deshabilitan esta opción por defecto para aquellos lenguajes que normalmente no utilizan excepciones (C por ejemplo), pero lo habilitan por defecto para los que, como C++, suelen utilizarlo.

  Inicio.


[1]   Parte de las librerías de C++Builder, la denominadas VCL "Visual Component Library" ( 4.11.8b), han sido desarrolladas en Pascal, y no forman parte de su Librería Estándar C++. Son extensiones particulares de este compilador.

[2]  Una opción muy bonita sería que llegado a este punto, el programa pudiera mandar un mensaje e-mail al programador o supervisor avisando del fallo (hoy día esto es trivial desde el punto de vista técnico).

[3]  El mecanismo de excepciones fue introducido en C++ en 1989. De hecho, una de las razones de su inclusión fue la dificultad para detectar y controlar los errores ocurridos en los constructores, ya que por definición estos métodos no devuelven nada.

[5]  El Dr. Stroustrup admite que, además de servir para el control de errores, las excepciones pueden servir como estructura de control en situaciones que no sean necesariamente erróneas TC++PL  §14.5  "Alternatively, one migth think of the exception-handling mechanisms as simply another control structure". Stroustrup & Ellis:  ACRM  §15.1  "This design assumes that exceptions are to be used primarily for error handling. Alternative uses, such as loop termination and alternate 'normal' return paths form functions, are clearly possible, but are considered secondary". 

[6]  Se recomienda utilizar una subclase (definida al efecto) de la superclase exception definida en la librería Estándar ( 1.6.1a).