Gestión de excepciones

C++ permite una gestión de errores basada en excepciones: las excepciones señalan errores, y se lanzan (throw), hasta que, en alguna función (donde se produjo el error o alguna desde donde se llamó a la función donde se produjo el error), se capturan (catch, dentro de un bloque try ... catch). Las excepciones se pueden distinguir a la hora de capturarlas, ya que se pueden poner varios catch en un bloque try..catch. Los parámetros de catch actuan como los de una función, convirtiéndose en parámetros formales.

Un ejemplo sencillo:

int main()
{
int a;
int b;
string s;

try {
cout << "Introduzca un número (dividendo):" << endl;
cin >> a;
cout << "Introduzca otro número (divisor):" << endl;
cin >> b;

if ( b != 0 )
cout << "La división es " << a/b << endl;
else throw 0;

cout << "Dame tu nombre:" << endl;
getline( cin, s );

if ( s.empty() ) {
throw s;
}

cout << "Tu nombre es " << s << endl;

}
catch(int e)
{
cerr << "Es necesario un divisor distinto de 0." << endl;
}
catch(const string &e)
{
cerr << "Es imposible que no tengas un nombre" << endl;
}
}


Aunque este no es el mejor uso posible de las excepciones, ilustra el hecho de que se pueden lanzar varias excepciones y distinguirlas en el bloque try ... catch.

En realidad, lo más correcto es definir nuestras propias clases de excepciones para poder lanzarlas instanciadas (en lugar de tipos primitivos como "int"), y que estos objetos nos dén mucha más información. Ya que tenemos una serie de clases predefinidas en la librería estándar, podemos darles uso. Incluyendo las cabeceras exception y stdexcept, podemos acceder a la clase raiz de las clases de excepciones en C++, exception, y la clase que probablemente es la mejor para "colgar" de ella nuestras propias clases de excepciones, runtime_error.

El ejemplo de antes, pero empleando nuestras propias clases:



#include <exception>
#include <stdexcept>
#include <iostream>

using namespace std;

class EDivisionPorCero : runtime_error {
public:
EDivisorCero(char *msg) : runtime_error( msg )
{}
};

class ECadenaVacia: runtime_error {
public:
ECadenaVacia(char *msg) : runtime_error( msg )
{}
};


int main()
{
string s;

try {
int * a = new int;
int * b = new int;

cout << "Introduzca un número (dividendo):" << endl;
cin >> *a;
cout << "Introduzca otro número (divisor):" << endl;
cin >> *b;

if ( *b != 0 )
cout << "La división es " << ( (*a) / (*b) ) << endl;
else throw EDivisionPorCero( "El divisor no puede ser cero" );

cout << "Dame tu nombre:" << endl;
getline( cin, s );

if ( s.empty() ) {
throw ECadenaVacia( "Tu nombre no puede ser vacío" );
}

cout << "Tu nombre es " << s << endl;

}
catch(const EDivisionPorCero &e)
{
cerr << "ERROR: " << e.what() << endl;
}
catch(const ECadenaVacia &e)
{
cerr << "ERROR: " << e.what() << endl;
}
catch(const exception &e)
{
cerr << "ERROR inesperado: " << e.what() << endl;
}
catch(...)
{
cerr << "ERROR fatal inesperado."
}
}


El orden en el que se definen los catch es importante: éste debe ser siempre de arriba a abajo, de lo más concreto a lo más general. Sí, por ejemplo, se hubiese puesto el catch de exception antes que el catch de ECadenaVacia, este último nunca se ejecutaría puesto que siempre entraría antes por el de exception.

El catch de exception captura cualquier posible excepción producido por la librería estándar. Por ejemplo, el operador "new" lanza la excepción bad_alloc, que sería capturada por este catch, cuando no hay suficiente memoria.

El catch más genérico es el catch(...), en él vale cualquier argumento posible. Por eso, si se incluye, siempre debe ser el último.

En realidad, esta es sólo una parte de la potencia de la gestión de excepciones. Lo más habitual no es que la excepción se produzca en la misma función o método.

Gestión de excepciones: recorrido del stack

Como decíamos en el anterior post, lo más normal no es que la excepción se produzca y se capture en la misma función/método, sino que el sistema deba recorrer la pila de llamadas hasta encontrar una función llamadora que tenga un catch que corresponda:


#include <exception>
#include <stdexcept>
#include <iostream>

using namespace std;

class EDivisionPorCero : runtime_error {
public:
EDivisorCero(char *msg) : runtime_error( msg )
{}
};

class ECadenaVacia: runtime_error {
public:
ECadenaVacia(char *msg) : runtime_error( msg )
{}
};

int divide(int a, int b) throw EDivisionPorCero
{
if ( b != 0 )
return a/b;
else throw EDivisionPorCero( "El divisor no puede ser cero" );
}

int main()
{
string s;

try {
int * a = new int;
int * b = new int;

cout << "Introduzca un número (dividendo):" << endl;
cin >> *a;
cout << "Introduzca otro número (divisor):" << endl;
cin >> *b;

cout << "La división es: " << divide( *a, *b ) << endl;

cout << "Dame tu nombre:" << endl;
getline( cin, s );

if ( s.empty() ) {
throw ECadenaVacia( "Tu nombre no puede ser vacío" );
}

cout << "Tu nombre es " << s << endl;

}
catch(const EDivisionPorCero &e)
{
cerr << "ERROR: " << e.what() << endl;
}
catch(const ECadenaVacia &e)
{
cerr << "ERROR: " << e.what() << endl;
}
catch(const exception &e)
{
cerr << "ERROR inesperado: " << e.what() << endl;
}
catch(...)
{
cerr << "ERROR fatal inesperado."
}
}


La función divide() es la que lanza la excepción, que es recogida en el main(), por el catch adecuado (el de EDivisionPorCero). La cláusula throw en la cabecera de la función indica las excepciones que puede lanzar (en este caso, sólo una), separadas por comas. Si la función lanzase una excepción distinta de las listadas, se produciría un error de compilación.

Esta cláusula es opcional, si no se pone, no tiene ningún efecto.