experiencia
Servicios - Sepa lo que Ricardo Devis & Asociados pueden hacer por ustedPublicaciones - Consulte los documentos que ponemos a su disposiciónContacto - Conozca como ponerse en contacto con Ricardo Devis & Asociados
 


RPP febrero 1996

Manejo de Excepciones en C++


Ricardo Devis
Botella


C++ proporciona soporte explícito para el tratamiento de las excepciones que puedan surgir en el flujo normal de computación de una aplicación dada, pero la sintaxis, comportamiento y, sobre todo, implementación de las soluciones provistas son relativamente nuevos. C++ es, por otro lado, un lenguaje excepcionalmente prolijo en sutilezas, así que parece conveniente una revisión del fondo y forma de las excepciones, que en esta ocasión se convierten en regla.

 

¿Excepciones? ¿Es que no basta con las reglas [1]? ¡Oh, difícilmente las construcciones humanas pueden tratar con la inflexible y perfecta exactitud reglada! Ya decía Goethe que “apenas hablamos, empezamos ya a equivocarnos”. Y si la vida está repleta de errores, equívocos e imponderables, imagínense el software, pobre y desdibujado reflejo de aquélla. Así que para manejarnos con cierta soltura debemos reglar también lo que no cabe en la propia norma, estableciendo ciertas estructuras de control que nos permitan, cuando menos, construir una somera malla en que las excepciones queden atrapadas: lo excepcional se convierte así en previsible, en alguna medida. Esta es la ventaja, claro. Pero desafortunadamente el diseño e implementación de un sistema de captación y manejo de excepciones posee también sus reglas, sutilezas y desventajas. Si encima añadimos a éstas la prolijidad y los problemas usualmente achacados al lenguaje C++, bueno, el asunto adquiere tintes inopinadamente malignos. Pero quizá el tratamiento de excepciones constituya una excepción respecto del comportamiento usual de C++: la sintaxis es relativamente sencilla, pero su aplicación en sistemas software reales requiere de una estrategia previa cuidadosamente medida (y más vale que el lector me crea a pies juntillas). Entre ambos extremos se sitúan, empero, los aspectos semánticos de uso de las características del lenguaje asociadas al manejo de excepciones y que son de los que este artículo se ocupará.

LOS CÓDIGOS DE ERROR

La mayoría de las estrategias de control de excepciones/errores/imprevistos se basan en que todo procedimiento/función/mensaje debe devolver a su usuario, considerado en un sentido amplio, un valor indicativo de su exito o fracaso. Así, por ejemplo, es común encontrar código como el siguiente:

int adiciona( Plantilla& plantilla, const Empleado& empleado )

{

if plantilla.incluye( empleado )

return -21545; //absurdo código de error

else {

plantilla.adiciona( empleado );

return 1;

}

}

de manera que pueda hacerse el siguiente uso de la función:

if ( !adiciona( plantilla, fernandez ) )

muestraDialogoError( “Empleado ya existe” );

Naturalmente lo que aquí se plantea resulta relativamente simple cuando se trata de funciones que debieran devolver void. Cuando no es así las cosas se complican:

Politico& PartidoPolitico::politicoHonrado()

{

// devuelve un político honrado del partido

// El problema es que si tal político no existe

// ¿qué se devuelve?

}

Una posible solución pasaría por reconvertir la función en otra que devuelva un codigo de error:

int PartidoPolitico::politicoHonrado( Politico* politico )

{

// si existe, asigna el politico encontrado al

// puntero que se le pasa, y devuelve 1

// si no existe devuelve ‘0’ ó algún valor negativo

}

de tal manera que el código cliente deberá siempre comprobar el valor de retorno antes de operar con el posible Politico encontrado, pues si realmente no se encuentra (el caso más frecuente) tal puntero apuntará ... ¿a qué?. Humm, esta solución, aparte de no ser especialmente elegante y resultar intrusiva [2], ni siquiera puede aplicarse en todas las situaciones: pensemos, por ejemplo, en los constructores, que no pueden devolver código alguno.

X

La devolución de códigos de error por funciones no puede aplicarse en muchas situaciones y, por tanto, no debe constituirse en estrategia genérica de manejo de excepciones.

La clara alternativa sería construir/devolver un objeto con un cierto estado interno relativamente estable y que, a la vez, indicara la “inestabilidad” del mismo: o sea, un objecto zombi:

class Persona {

long dni;

public:

Persona::Persona(

if ( unDNI < 0 ) {

dni = 0;

return;

}

// código consructor persona

}

int comprueba() {

return dni;

}

// sigue resto descripción de clase

};

Pero esto arrostra nuevos problemas: ahora obligaremos bien a los clientes de la clase a validar cada objeto antes de usarlo:

if ( fulano.comprueba() )

cout << fulano;

bien a incluir código de comprobación en cada función miembro de la clase Persona, que a su vez deberían devolver, así, ciertos códigos de error.

X

Los objetos “zombis” generan una ingente cantidad de código cliente de comprobación y bifurcación usualmente dependiente de unas especificaciones volátiles. La aplicación indiscriminada de este enfoque ocasiona sistemas software de dudosa mantenibilidad.

¿Adivina ahora el lector por qué las estrategias usuales de error suelen resultar tan artificiosas, frágiles y díficiles de mantener? Basta con echarle un vistazo a los manuales corporativos de referencia de códigos de error: una barbaridad tal que al final proporciona ciertos gusto y adicción malsanos [3]. Naturalmente no se afirma aquí que los códigos de error resulten dañinos per se, pues de alguna manera (con algún código) ha de comunicarse la parte que genera un error con la parte que la maneja. Pero el cómo es fundamental.

CONCEPTOS BÁSICOS

Como solución a estas cuitas, lo que C++ provee son tres nuevas construcciones explicitadas en tres nuevas palabras reservadas:

  • la expresión throw [4], que lanza excepciones.
  • el bloque catch, que define manejadores de excepciones.
  • el bloque try, que indica las secciones de código que son sensitivas respecto de las excepciones.

y su funcionamiento, en breve, es el siguiente: en cualquier sección del código puede lanzarse, mediante throw, una excepción, pero tal excepción sólo será procesada si tal sección está incluida, directa o indirectamente, en un bloque try, y de tal proceso se encargarán los manejadores definidos por catch y afectos al bloque try en que se incluya la excepción lanzada.

La expresión throw puede aparecer en cualquier parte del código de la siguiente guisa:

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*

mientras que el bloque catch ha de ir forzosamente antecedido por un bloque try, de la misma manera que un else ha de ir precedido de un if. Pero tras un try (inmediatamente después) pueden asociarse varios bloques catch. El bloque try no posee argumentos, pues su propósito es indicar sobre qué porción de código (la encerrada entre las llaves del try) se aplicarán los manejadores catch que le siguen. Los manejadores catch, sin embargo, sí poseen argumentos: desde la elipsis (...) hasta cualquier tipo definido-por-el-usuario:

try {

// una porción de código cualquiera

}

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 );

}

El manejo de excepciones en C++ es, en esencia, un mecanismo que permite la comunicación entre dos partes distintas de una aplicación (y esto provoca no pocas críticas, como más adelante veremos, al estar tales partes usualmente bien distantes). En realidad hay una cierta similitud entre tal mecanismo y el de llamada y resolución de funciones: la expresión throw equivaldría a la llamada a la función, con un argumento dado, mientras que el bloque catch representaría la definición de la función. La diferencia fundamental consiste en que la ligazón de una llamada a una función con el cuerpo de ésta se produce en tiempo de compilación, mientras que la resolución del mecanismo de manejo de excepciones se produce en tiempo de ejecución. Esto significa que el compilador no sabe qué bloque catch (si acaso alguno) manejará una determinada excepción. De hecho lo único que puede hacer el compilador es generar ciertas estructuras que soporten la información necesaria para que funcione el mecanismo en tiempo de ejecución. Así cada vez que el compilador encuentra una expresión throw o catch crearía un “descriptor de tipo” para cada una de ellas, permitiendo su comparación en tiempo de ejecución. El encaje de ambos descriptores es, en gran medida, igual al encaje de argumentos en funciones: un operando de throw con tipo “E” casará con un manejador catch con argumento de tipo “M”, “const M”, “M&” o “const M&” si:

  • E y M son del mismo tipo
  • M es una clase base inambigüa de E
  • M y E son punteros y se da una conversión estándar de E a M

Resulta, entonces, que si se lanza la siguiente excepción, significada por un objeto que se construye para la ocasión:

throw Mensaje( “That’s all folks” );

la expresión throw inicializaría un objeto temporal del tipo estático del operando (éste es, “Mensaje”), y tal objeto se usaría a su vez para inicializar, de la misma manera que un argumento en la llamada a una función, la variable establecida como operando del catch adecuado. Vamos, que el mecanismo semeja el de una expresión return. En tal sentido debe tenerse en cuenta que el paso por valor significa el uso del constructor de copia, y que a pesar que la memoria para el objeto temporal se asigna de una forma dependiente de la implementación, el destructor del objeto deberá ser finalmente usado, de manera que tal funcionalidad mínima tendrá que ser prevista en la clase.

V

Los objetos que se utilicen como operandos de expresiones throw deben pertenecer a clases en que el constructor de copia y el destructor sean accesibles (estén declarados en la sección pública).

Cuando se “entra” en un manejador catch que toma como operando un objeto (no una referencia a un objeto) de una clase dada, se produce una copia del objeto lanzado por la expresión throw correspondiente, que se destruye cuando se sale del ámbito del manejador. Si el catch espera un objeto no-constante, los posibles cambios realizados en la copia del objeto dentro del manejador serán locales a dicha copia y, por tanto, se perderán a la salida del catch.

MANOS A LA OBRA

Seguidamente vamos a detallar un ejemplo elemental en el que se han señalado las líneas de interés con números para permitir su rápida localización en la densa explicación que sigue al código:

class MensajeDeSocorro {

public:

MensajeDeSocorro( char* cadena );

char* cadena() const;

// resto descripción clase

};

 

void Programador::codifica()

{

desactivaSalvaPantallas();

escribe();

escribe();

escribe();

modifica();

piensa();

throw MensajeDeSocorro( “¿Qué había que hacer?” ); // (1)

}

 

extern Programador* programador;

void Analista::analiza()

{

try { // (2)

desactivaSalvaPantallas();

escribe();

escribe();

escribe();

discute();

piensa();

programador->codifica();

throw “¿Qué había que hacer?”; // (3)

}

catch ( char* ) { // (4)

VentanaModalDeAviso( “Cambiar de analista” );

}

}

 

extern Analista* analista;

void empiezaProyecto()

{

try { // (5)

analista->analiza();

}

catch( ... ) { // (6)

throw;

}

catch( MensajeDeSocorro mensaje ) { // (7)

cout << “Esta línea es inalcanzable”;

}

}

 

void ejecutaProyecto() //nunca mejor dicho

{

try { // (8)

empiezaProyecto();

terminaProyecto();

}

catch (MensajeDeSocorro mensaje ) { // (9)

cout << mensaje.cadena();

}

}

Empecemos con la última función “ejecutaProyecto()”: en primer lugar se llama a la función “empiezaProyecto()”, que a su vez llama a la función “Analista::analiza()”, que llama a la función “Programador::codifica()” que genera una excepción del tipo “MensajeDeSocorro” lanzando un objeto de tal clase construido con un argumento de tipo “String” (línea 1). Bien. Ahora sólo hay que ver cómo se maneja esta excepción.

Obviando otras consideraciones, lo primero que se hace es mirar si la excepción está contenida directamente en un bloque try. Como no es así, se busca en el ámbito en que se llama a la función “Programador::codifica()”, dentro de la función “Programador::analiza()”, que sí está incluida en un bloque try (línea 2). Seguidamente miramos si el bloque try tiene uno o más bloques catch asociados. Y así es: en la línea 4 se da un catch que admite objetos del tipo “char*”. Pero el objeto que se lanzó en el throw era de tipo “MensajeDeSocorro”, por lo que este catch no es de aplicación. Así que se busca un nuevo bloque try exterior, que se encuentra en la línea 5 y que tiene asociados dos bloques catch en las líneas 6 y 7. En seguida se comienza la comparación del tipo lanzado con el que espera cada uno de los bloques catch, examinados en estricto orden de aparición. Así tenemos que el primero (línea 6) admite cualquier tipo de argumento (...), por lo que casa perfectamente con el tipo “MensajeDeSocorro” que se lanzó en el throw inicial. Pasa inmediatamente a ejecutarse el catch(...), y en ese mismo momento la excepción se entiende manejada (exactamente así, querido lector). Lo que sigue es una instrucción “throw” sin más que, simplemente, relanza la misma excepción que había captado, significada en el mismo objeto de tipo “MensajeDeSocorro”: o sea, es como si se repitiera la línea 1, pero en esta nueva posición:

!

Una expresión throw sin operando relanza la excepción que en ese momento se estaba manejando sin copiarla. Si no se está manejando ninguna excepción se genera una llamada a “terminate()”.

Claro que ahora el lector despistado podría pensar: “Bueno, perfecto. El segundo catch de la línea 7, que espera un objeto de tipo “MensajeDeSocorro” manejará perfectamente la excepción relanzada”. ¡En absoluto! Hay que pensar que las conjunciones “try-catch” se asemejan sobremanera en su comportamiento a las conjunciones “switch-case” (suponiendo un break al final de cada case): para cada try se ejecuta, a lo sumo, solo uno de los catch asociados. Es fácil ver, así, que como el catch de la línea 6 capta cualquier excepción de cualquier tipo, el catch de la línea 7 no será alcanzado por ninguna excepción en ningún caso, y de hecho un buen compilador emitiría el siguiente error (sí caro lector: error, no aviso):

error EDC3194: "catch(MensajeDeSocorro)"

will never be reached because of previous "catch(...)".

V

Si siguiendo a un bloque try se da un catch(...), éste debe ocupar el último lugar entre los manejadores.

Pero volvamos al flujo de nuestra excepción. Como ya se ha ejecutado el catch(...) al relanzar la excepción se vuelve a buscar un bloque try en un ámbito exterior, que se encuentra en la línea 8, dentro de la función “ejecutaProyecto()”. Este try tiene asociado un solo catch (línea 9) que admite el tipo “MensajeDeSocorro”, y por tanto casa con el tipo de nuestra excepción, por lo que tal catch la manejará. Y como en este bloque catch se explicita un identificador para el objeto (que no aparecía en el catch de la línea 7), puede usarse tal objeto en su interior: en este caso para llamar a una función miembro, “cadena()”, que devuelve la cadena de caracteres de su representación interna (ésta es, “¿Qué había que hacer?”). Recapacite el lector que este último catch (línea 9) admite un objeto (no una referencia) de forma que, como ya se notó en el parágrafo anterior, si se produjera algún cambio en el mismo, éste se perdería al salir del ámbito del manejador. Hay que fomentar aquí, también, las mismas consideraciones de eficiencia que aconsejan el uso de referencias a objetos como argumentos de funciones.

Incidentalmente el lector habrá también notado que la línea 3, en que está codificado directamente el lanzamiento de una excepción asociada a la función “Analista::analiza()”, nunca será alcanzada, pues la excepción lanzada con anterioridad por la función “Programador::codifica()” modifica el flujo secuencial del programa, como es fácil suponer y más adelante veremos. De esta manera el software imita al mundo real: el error del programador impide que el del analista resulte visible.

CLASES DE EXCEPCIONES

El lector habrá apreciado que en el ejemplo anterior se lanzan tanto objetos de tipo predefinido como objetos de clases definidas-por-el-usuario. Lo que el lector habrá también asimilado a estas alturas es que las clases de excepciones se corresponden con las clases (por tipos) de los objetos lanzados como excepciones. Resulta, así, que en una estrategia de manejo de excepciones en C++ es imprescindible la creación de un cúmulo de clases adecuadas para tratar cada error. Naturalmente podemos crear clases independientes para cada excepción, pero ¿por qué no usar del mecanismo de la herencia que provee C++ y construir una jerarquía de clases de error? Una de las principales ventajas (discutidas, no obstante, por ciertos autores y organizaciones) de este enfoque es el tratamiento uniforme que se daría a la especificación de excepciones, que examinaremos en breve. Pero no adelantemos acontecimientos y fiémonos ahora simplemente de las supuestas bondades universales de la derivación como mecanismo “bueno-para-casi-todo”.

JERARQUÍAS DE CLASES DE EXCEPCIONES

Si construimos una jerarquía de clases representativas de excepciones/errores, parece prudente que la clase base (TStandardException, por ejemplo, en el caso del Taligent Application Environment) contenga una mínima funcionalidad (o ninguna, si se piensa que el estándar del lenguaje puede llegar a reglar este extremo) más un destructor virtual (imprescindible en las jerarquías de derivación, como el lector ya debería saber). De cualquier forma y en aras de la pedagogía examinaremos un ejemplo más simple:

class Error { /* clase base */ };

class ErrorBaseDeDatos : public Error { /*...*/ };

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

void actualizaClientes() {

throw ErrorDeActualizacion(); // (10)

}

try { // (11)

actualizaClientes();

}

catch ( ErrorDeActualizacion ) { /*...*/ } // (12)

catch ( Error ) { /*...*/ } // (13)

catch ( ErrorBaseDeDatos ) { /*...*/ } // (14)

Como vimos anteriormente, la excepción lanzada en la línea 10 salta hasta el bloque try de la línea 11 y seguidamente pasa a comprobar sus manejadores asociados. Como éstos se examinan en estricto orden de aparición, es fácil ver que el primero de ellos (línea 12) es del mismo tipo que la excepción lanzada, y por tanto casa perfectamente con ella. Las cosas cambiarían, sin embargo, si la excepción lanzada fuera

void actualizaClientes() {

throw ErrorBaseDeDatos();

}

pues al empezar la comparación de descriptores de tipo con los de los bloques catch encontraría que el de la línea 12 no casa, pero sí lo hace el siguiente de la línea 13, ya que el operando de la expresión throw es un objeto de una clase derivada públicamente de “Error”. De aquí se sigue, como el lector ya se está apresurando a proclamar, que la línea 14 no será alcanzada en ningún caso.

V

Si en un conjunto de bloques catch tras un bloque try se manejan tipos de datos en jerarquía, los bloques catch con argumento de clases derivadas deberán siempre anteceder a los catch que manejen excepciones de sus clases base respectivas..

EL DESBOBINADO [5] DE LA PILA

Cuando se lanza una excepción el programa “salta” hacia un nivel superior donde el error podrá ser procesado [6] o, lo que es lo mismo, se transfiere el control de la ejecución del programa al primer bloque try que lo encierre. Normalmente los objetos en la pila (stack) se destruyen cuando finaliza el ámbito en que fueron declarados, pero al realizar el “salto” quedarían objetos abandonados entre el lanzamiento y el manejador. Para evitar esto se invocan los destructores de todos los objetos automáticos construidos hasta que se encuentra el bloque try, y este proceso de denomina “desbobinado de la pila” (stack unwinding). Pero pongamos las cosas más difíciles: imaginemos que se genera una excepción en el bloque de inicialización de un constructor, de tal forma que algunos de los objetos que debería inicializar quedan sin construir. El desbobinado de la pila, a resultas de tal excepción, llamará solamente a los destructores de los objetos totalmente construidos.

CONSISTENCIA DE ESTADOS

Stroustrup relata con detalle las dudas que surgieron en el proceso de diseño del mecanismo de manejo de excepciones en C++ respecto de sí debía ser continuativo o conclusivo: o sea, si tal mecanismo debía constituirse en una bifucarción que, tras ser tratada, pudiera devolver el flujo del programa a donde se lanzó la excepción o bien debería adoptarse el actual enfoque. La decisión final es actualmente evidente. Las razones fueron, en esencia, que el mecanismo continuativo representaba graves complejidades como estrategia y, sin embargo, resultaba fácilmente codificable tomando como base el enfoque conclusivo.

Es relativamente usual, por ejemplo, que una función miembro necesite completar una serie de acciones para mantener la consistencia del estado interno del objeto a que se aplica. Echemos, si no, un vistazo a la siguiente función:

void Politico::adjudicaContrato( long soborno )

{

recibeDinero( soborno );

cometeTurbiosManejos();

emiteDictamenFavorable();

}

El problema es que si en la funcion “cometeTurbiosManejos()” se genera una excepción, entonces no se alcanzará nunca la función “emiteDictamenFavorable()” y el político, sin embargo, se habrá embolsado el dinero del soborno. Pero esto, aunque refleja fielmente la realidad, no parece convenir comercialmente. ¿Cómo solucionar, pues, este claro problema de contraprestaciones?

void PoliticoHonrado::adjudicaContrato(

Adjudicatario& X, long soborno )

{

recibeDinero( soborno );

try {

cometeTurbiosManejos();

}

catch(...) {

devuelveDinero( X, soborno );

throw; // relanza la excepción

}

emiteDictamenFavorable();

}

Claro que aquí el lector descontento podría exclamar: “Oh, esto es demasiado prolijo. Si tengo que codificar de esta manera todas mis funciones el código final crecerá una barbaridad. Y eso sin hablar de los errores tipográficos que acechan en cada línea”. Bueno, en primer lugar he de reconocer que el lector se expresa muy bien, para después proclamar que, como ya anuncié, el manejo de excepciones en C++ no es tarea simple.

ADQUISICIÓN DE RECURSOS VÍA INICIALIZACIÓN

Otra cuestión frecuente es la que se refiere a la liberación de los recursos adquiridos. Veamos un ejemplo de la vida real:

void Politico::cobra()

{

HombreDePaja* testaferro = new HombreDePaja();

// Aquí se suceden distintas llamadas

// a funciones y métodos (a cuál peor)

delete testaferro; // se eliminan las pruebas

}

El problema es que si entre la creación del “testaferro” y su destrucción se genera alguna excepción, la memoria asignada (primaria o secundaria, si incluimos la persistencia) nunca se liberará. Naturalmente la solución podría pasar por un esquema similar al del parágrafo anterior, insertando una expresión try-catch entre la creación y destrucción del objeto, pero una mejor solución sería crear una clase

class PunteroAHombreDePaja {

public:

PunteroAHombreDePaja() : testaferro( new HombreDePaja; ) {}

~PunteroAHombreDePaja() { delete testaferro; }

HombreDePaja* testaferro; // ¡anatema!

};

de tal manera que la función se recodificaría de la siguiente guisa:

void Politico::cobra()

{

PunteroAHombreDePaja testaferro;

// Transacciones financieras varias

}

Si no se produce una excepción tras la creación de “testaferro”, al salir del ámbito de la función se llamará al destructor del objeto “testaferro” y al ejecutarse éste se liberará la memoria dinámica asignada. Si se genera una excepción tras la creación del objeto, al ponerse en marcha el desbobinado de la pila se llamará también al destructor, obteniendo el mismo resultado.

Este planteamiento, resultando efectivo, no es, sin embargo, suficientemente satisfactorio: por un lado tenemos que el código original operaba con punteros, y éste lo hace con objetos, de manera que bien habría que cambiar el código (lo que frecuentemente es inaceptable) bien dotar a la clase con una función u operador* que devolviese el puntero para así poder operar con él; por otro lado resulta que con un tal esquema habría que crear una clase distinta para cada tipo de dato que se deseara manejar con esta técnica. La solución es echar mano de las plantillas:

template <class T >

class New {

public:

New() : t( new T ) {}

~New() { delete t; }

New( T* tt ) { t = tt; }

operator T*() { return t; }

private:

T* t;

protected:

// se declaran no-públicos el constructor de copia y

// el operador de asignación para evitar problemas

New( const New& n );

const New& operator=( const New& n );

};

De manera que ahora podríamos escribir:

class HombreDePaja : public PersonaFisica { /* lo que sea */ };

void firma( HombreDePaja* hdp ) { /* ... */ }

void Politico::cobra()

{

New< HombreDePaja > testaferro;

// ahora puede usarse testaferro como un puntero

// a un HombreDePaja, merced al operator*.

firma( testaferro );

}

Otra solución, a falta de un buen recolector de basura para C++, sería usar de Punteros Inteligentes (Smart Pointers) para gestionar el manejo de excepciones. Pero esto se sale del ámbito de lo aquí pretendido, así que el lector interesado puede echarle un vistazo al interesante artículo de Steve Churchill.

ESPECIFICACIÓN DE INTERFACES

Resulta desafortunadamente usual que un programador, a poco de aprender (oh, ¿dije aprender?) alguna nueva característica de un lenguaje, proceda bien a obviarla bien a aplicarla sin control ni concierto. Imaginen el desbarajuste que puede ocasionar un programador(de los hiperactivos: segundo caso) lanzando excepciones a diestro y siniestro. Ahora magnifiquen el desastre: piénsense usando el código así generado por otra persona. Naturalmente los programas generados, al no tener claro conocimiento de las excepciones lanzadas (pues el código de implementación de las funciones generalmente no es accesible), no podrán manejarlas prudentemente, así que todo serán abruptas finalizaciones. ¿No habría alguna manera de especificar, de una forma expresa y clara, el tipo de excepciones que una función puede lanzar? ¡Pues claro! Para eso está la especificación de excepciones.

La idea es matizadamente simple: en el declarador de una función bien en su declaración bien en su definición (pero no en ambas, como tampoco en un typedef) puede detallarse la lista de tipos de excepciones que tal función puede legalmente lanzar. Veamos unos ejemplos:

void f() throw( int );

int g() throw( Overflow, ErrorDB );

Poltico& encarcelar() throw ( PoliticoHonesto );

Para evitar un cambio impensable en todo el código existente, la no declaración de una especificación de excepciones equivale a:

void funcionNormal() throw( ... );

En justa correspondencia, para especificar que una función no puede legalmente lanzar ninguna excepción se escribe:

void funcionSinExcepciones() throw();

Bueno, esto resulta perfecto: ahora cada uno sabrá a qué atenerse cuando maneja funciones ajenas, dotando a su código si es necesario de los manejadores adecuados (y antes de los bloques try) para controlar las posibles excepciones. ¿Perfecto? Humm, seguro que el lector ha notado un cierto retintín en la repetición del adverbio “legalmente”. ¿Qué significa aquí está apreciación jurídica? Pues exactamente lo siguiente: si una función con una especificación de excepciones de ciertos tipos lanza una excepción no contemplada en la lista, entonces el sistema llamará automáticamente a la función “unexpected()”, que por defecto llamará a “terminate()”, que por defecto, a su vez, llamará a “abort()”, y el programa acabará abruptamente. ¡Vaya! ¡Funciones nuevas! Echémosles un vistazo.

EL FINAL DE LA CUERDA

C++ provee dos funciones esenciales para el manejo básico de excepciones, incluidas en la cabecera <exception> de la biblioteca estándar del lenguaje: “void terminate()” y “void unexpected()”. Veamos cada una por separado.

La función “terminate()” se llama, en breve, cuando una excepción no encuentra manejador, cuando se genera una excepción en el proceso de destrucción de un objeto en el desbobinado de la pila (stack unwinding), o cuando se da algún error interno.

XXX

No se debe lanzar una excepción desde el interior del cuerpo de un destructor, pues si su lanzamiento se produce durante un desbobinado de la pila se ignorará el mecanismo de manejo de errores y se llamará indefectiblemente a “terminate()”.

La función “terminate()” llama a su función manejadora asociada de tipo

typedef void (*terminate_handler)();

y cuyo comportamiento por defecto es llamar a la función “abort()”. El manejador asociado a “terminate()” puede, no obstante, ser cambiado haciendo uso de la función

terminate_handler set_terminate( terminate_handler f ) throw();

que admite un puntero no nulo a una función (que necesariamente debe terminar sin devolver el control al usuario) y retorna el anterior manejador.

VULNERACIÓN DE LA ESPECIFICACIÓN DE EXCEPCIONES

Si una función con una especificación de excepciones asociada lanza una excepción no contemplada en tal lista, se producirá una llamada a la función “void unexpected()”, que a su vez llamará a su manejador asociado de tipo

typedef void (*unexpected_handler)();

que por defecto llamará a la función “terminate()”. Tal manejador puede cambiarse, empero,mediante la función

unexpected_handler set_unexpected( unexpected_handler f ) throw();

que devuelve el manejador hasta entonces válido y admite un nuevo puntero no nulo a una función que puede lanzar una excepción ya normal ya de tipo bad_exception, o bien llamar a “exit()” o “abort()”

¡Demonios! ¡Otra nueva palabra! Bueno, sufrido lector, no me negará que esta situación excepcional lo disculpa casi todo: “bad_exception” es el nombre de una clase derivada públicamente de “exception”, perteneciente a la biblioteca estándar del lenguaje, y cuya razón de ser es precisamente evitar el problema sugerido por Taligent y que se refiere al peligro de usar funciones con especificación de excepciones en un código robusto: cuando se lanza una excepción no prevista, directa o indirectamente, la acción por defecto es terminar el programa. Y, claro, esto resulta inaceptable. Debe procurarse algún mecanismo que permita obviar conscientemente este problema sin eliminar las útiles especificaciones de interfaz. La solución la proporciona la clase “bad_exception”. Pero veámoslo en detalle.

Si el tipo de la excepción lanzada por una función no está en la lista de especificaciones de ésta, entonces, como el lector bien sabe, se llama a unexpected(). Si “unexpected()” lanza a su vez una excepción permitida en tal lista, entonces continúa la búsqueda de manejador en el ámbito de la llamada a la función primera. Si, por el contrario, el manejador de “unexpected()” lanza una excepción de un tipo no permitido (no incluido en la lista), si acaso no se había incluido el tipo “bad_exception” en la especificación, se llamará igualmente a “terminate()”; si contrariamente se había incluido, como por ejemplo en

void funcion() throw (int, Overflow, bad_exception );

entonces la excepción no permitida y lanzada se sustituye por un objeto, definido por cada implementación particular, del tipo “bad_exception” y se continúa la búsqueda de un nuevo manejador en el ámbito de la llamada a la función con la especificación referida.

CONCLUSIONES

Como bien afirma 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”. De hecho él mismo establece unas cuantas indicaciones de uso de excepciones:

  • Captar únicamente aquellos errores que se puedan manejar.
  • Las excepciones deben usarse en circunstancias excepcionales.
  • No apoyarse en excepciones si se puedes validar el código.
  • No lanzar una excepción si se puede continuar.
  • Permitir a los usuarios de bibliotecas decidir cómo desean que los errores sean manejados.
  • Usar excepciones cuando se den fallos en constructores.
  • No pierdas recursos durante el procesado de excepciones

David Reed, por su parte, expone un plan metódico para incorporar las excepciones a un código ya existente:

  • Implementar manejadores especiales para las funciones “terminate()” y “unexpected()”.
  • Añadir bloques try/catch que cubran las excepciones pre-existentes.
  • Diseñar y utilizar clases de excepciones.
  • Añadir especificaciones de excepciones a las funciones.
  • Localizar y reparar pérdidas de recursos.

Como el lector puede apreciar, el tema da para mucho: quedáronse muchas ideas y técnicas de uso de las excepciones en el procesador de textos, como el mantenimiento de invariantes, el tratamiento de precondiciones y postcondiciones, etc. etc.

!

Lea, lector, lea. Porque cualquier intento de aplicar el manejo de excepciones en C++ sin conocer exactamente los recursos del lenguaje y las características del compilador troca imposible la definición de una estrategia exitosa que estructure las excepciones en una arquitectura eficaz, efectiva y fácilmente mantenible. Lo contrario es, sin duda, una bomba de relojería gobernada por un reloj estropeado.

REFERENCIAS

  • Working Paper for Draft Proposed International Standard for Information Systems - Programming Language C++ , Documento X3J16/95-0185-WG21/N0785, 26 de septiembre 1995.
  • Exception Handling: Supporting the Runtime Mechanism , Josée Lajoie, SIGS Publications, C++ Report, marzo-abril 1994.
  • Using C++ Exceptions , David Reed, SIGS Publications, C++ Report, marzo-abril 1994.
  • Excepctions: Pragmatic issues with a new language feature , David R. Reed, SIGS Publications, C++ Report, octubre 1993.
  • Designing and Coding Reusable C++ , Martin D. Carroll & Margaret A. Ellis, 1995, Addison-Wesley, 0-201-51284-X.
  • The Design and Evolution of C++ , Bjarne Stroustrup, 1994, Addison-Wesley, 0-201-54330-3.
  • Mastering Object-Oriented Design in C++ , Cay S. Horstmann,1995, John Wiley & Sons, 0-471-59484-9.
  • C++ Strategies and Tactics , Robert B. Murray, 1993, Addison-Wesley, 0-201-56382-7.
  • Exception Recovery with Smart Pointers , Steve Churchill, SIGS Publications, C++ Report, enero 1994.
  • Taligent’s Guide to Designing Programs: Well-Mannered Object-Oriented Design in C++ , Taligent Inc, 1994, Addison-Wesley, 0-201-40888-0.
  • Designing with Exceptions , Grady Booch y Michael Vilot, SIGS Publications, C++ Report, Mayo 1994.
  • Designing with Exceptions , Grady Booch y Michael Vilot, SIGS Publications, C++ Report, julio-agosto 1994.
  • C++ Primer, 2nd Edition , Stanley B. Lippman,1991, Addison-Wesley, 0-201-54848-8.
The C++ Programming Language, 2nd Edition , Bjarne Stroustrup, 1991, Addison-Wesley, 0-201-53992-6.

 

[1] Quien piense que el proverbio latino Exceptio probat regulam significa “la excepción confirma la regla” debería repasar su latín.

[2] Cuando se quiere recomponer artificialmente código muerto, el producto final tiene bastantes posibilidades de acabar como el monstruo de Victor Frankentein: bien en la hoguera bien como leitmotiv de películas absolutamente infames. El lector morboso puede encontrar más referencias de este tipo en mi columna “Ingeniería de ... ¿qué?” en este mismo número.

[3] Des Esseintes, el sofisticado protagonista del “À rebours” de Huysmans, gustaba de coleccionar orquideas naturales que parecieran absolutamente artificiales: las corporaciones intentan alcanzar el aberrante nivel opuesto asimilando a cada error un código que resulte humanamente intuitivo (¡Diantre! ¡La psicología industrial se ha desquiciado!).

[4] Como señala Stroustrup, “raise” o “signal” hubieran sido palabras más apropiadas, pero éstas ya existían en la biblioteca estándar de C.

[5] Bueno, “desbobinado” no existe en castellano, pero rebobinado, el vocablo más ajustado, representa la acción de “desbobinar” para bobinar en otro carrete (un tipo de pila, al fin y al cabo). Por si el lector abriga alguna duda, “desempilar” o “desapilar” tampoco existen.

[6] ¿No les viene a la cabeza una poca piadosa semblanza con la actitud de algunos ante la supuesta vida eterna? ¡Yerra, yerra y sufre, que de todo se ocupará un nivel superior! Ah, pero, ¿y vivir? Esta es, naturalmente, una de las razones por las que el hombre mata a Dios (pero su sombra es larga, que decía Nietzsche).

 
 
 
volver a la página de publicaciones
 
 
 Pº. Castellana 188, 14º e · 28046 - Madrid · info@a4devis.com