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.