TEMA 9: EXCEPCIONES. PATRÓN RAII.
Contenidos
- 1. Definicion.
- 2. Tratamiento clásico de los errores (asertos).
- 3. Tratamiento de errores mediante el uso de excepciones.
- 4. Sintáxis empleada.
- 5. Características especiales de las excepciones en C++.
- 6. Jerarquía de clases de excepciones.
- 7. Patrón RAII.
- 8. ¿Existe algún otro modo de tratar los errores?
- 9. Aclaraciones
1. Definicion.
- Las excepciones se emplean en situaciones excepcionales que se producen en tiempo de ejecución y que no se pueden conocer en tiempo de compilación, cuando estamos desarrollando nuestra aplicación o biblioteca.
- Por tanto no se deben confundir con errores en la lógica de nuestro código, estos son otra cosa totalmente distinta.
Simply put, an Error is a bug in the application. An Exception is a bug in the input to the application. The former is not recoverable, the latter is.
– Walter Bright, D Language creator.
2. Tratamiento clásico de los errores (asertos).
2.1. Errores en la lógica del código.
- La detección de errores de lógica en nuestro código ya la conocemos
de Programación I, consiste en el uso de
assert
. assert(condition)
es una macro que evalúa un predicado, si este escierto
, el código sigue ejecutándose, pero si esfalso
se aborta la ejecución de la aplicación inmediatamente en ese punto, informándonos de dónde ha fallado la aserción y por qué motivo.
Para hacer uso de
assert
desde C++ incluiremos la cabeceracassert
.#include <iostream> #include <cassert> double fastSqrt (double x) { assert(x >= 0.0 ); ... assert(x == (result*result)); return result; }
2.2. Pero todo esto se puede hacer con sentencias if
, ¿no?
Por ejemplo, así:
#include <iostream> double fastSqrt (double x) { if (not (x >= 0.0) ) ... ; ... if (not (x == (result*result)) ... ; return result; }
- ¿Dónde está la ventaja entonces de
assert
? - Podemos deshabilitar fácilmente el chequeo de asertos en código
libre de errores de lógica definiendo al compilar la macro
NDEBUG
, p.e. así:g++ -DNDEBUG example.cc
.
3. Tratamiento de errores mediante el uso de excepciones.
- Son la alternativa al tratamiento de errores consistente en devolver un simple código de error (valor numérico).
- Aportan, por tanto, más información al punto de nuestro código donde se recibe el error.
- Pero, sobre todo en el paradigma de POO, donde podemos tener varios objetos vivos en el punto donde se produce el error y que por tanto deberían desaparecer liberando toda la memoria que pudieran tener reservada, nos permite automatizar este proceso.
- Para ello nos permiten deshacer la pila de llamadas, invocando los destructores de los objetos implicados; y ya sabemos lo que podemos/debemos hacer en el destructor de un objeto cuando se invoca.
4. Sintáxis empleada.
- C++ (y lenguajes como Java, C#, Vala, D, etc…) emplean una sintáxis similar.
- En C++ se emplean las palabras reservadas:
try
throw
catch
Un ejemplo muy sencillo:
#include <iostream> double f(double d) { if (d > 1e7) throw std::overflow_error("too big"); else return d; } int main() { try { std::cout << f(1e10) << '\n'; } catch (const std::overflow_error& e) { std::cout << e.what() << '\n'; } }
5. Características especiales de las excepciones en C++.
- Se permite lanzar excepciones de tipos base del lenguaje, p.e.,
char
,int
, etc… - Se puede indicar la lista de excepciones que puede lanzar una
función en C++ pre C++14:
void f() throw(int);
- Si una función no lanza ninguna se indica así:
- en C++ pre C++11:
void f() throw();
- en C++11 o posterior así:
void f() noexcept;
ovoid f() noexcept(true);
- en C++ pre C++11:
- Aquí tienes más información sobre noexcept.
- Si se lanza una excepción no especificada en la lista de posibles
excepciones, se llama a
std::unexpected()
. - Por defecto
unexpected
llama astd::terminate
y el programa acaba. Se puede cambiar la función a la que llama, p.e.:
#include <exception> // std::set_unexpected void myunexpected () { std::cerr << "unexpected called\n"; } int main() { std::set_unexpected(myunexpected); ... }
- Si una función no especifica una lista vacía de excepciones o
noexcept
, se asume que puede lanzar cualquier excepción.
A un bloque try le podemos asociar varios bloques catch:
int main (void) { std::set_unexpected (myunexpected); try { myfunction(); } catch (int) { std::cerr << "caught int\n"; } catch (...) { std::cerr << "caught some other exception type\n"; } return 0; }
IMPORTANTE: Los bloques catch se evalúan por orden, por tanto si el tipo de un catch concuerda con el tipo de la excepción lanzada, entonces se trata en ese catch.
class ExceptionBase {...}; class ExceptionDerived : public ExceptionBase {...}; ... try { f (); // throws ExceptionDerived } catch (ExceptionBase& eb) {...} // <-- se captura aquí catch (ExceptionDerived& ed) {...}
- Los bloques catch con elipsis (…):
catch (...)
capturan cualquier tipo de excepción, por eso se ponen al final. Una excepción se puede relanzar:
try { f (); // throws ExceptionDerived } catch (ExceptionBase& eb) { throw; } // <-- se relanza aquí
6. Jerarquía de clases de excepciones.
- C++ nos proporciona una serie de clases predefinidas que sirven para representar tipos de error en nuestro código.
- Debemos incluir la cabecera
stdexcept
(la cual incluye aexception
). - De este modo tenemos acceso a las clases
exception
y derivadas de ella comologic_error
,length_error
, etc…
- Todas ellas tienen un constructor a partir de una cadena, p.e.:
runtime_error( const std::string& what_arg );
. - En esta cadena podemos aprovechar para poner una descripción del error.
- Esta cadena puede ser consultada con el método heredado de la clase
exception
:
virtual const char* what() const noexcept;
7. Patrón RAII.
- Inventado por Bjarne Stroustrup (creador de C++).
Los recursos (memoria, ficheros, etc…) que usa un objeto se crean cuando este se inicializa (constructor) y se destruyen cuando el objeto desaparece (destructor).
class file { // https://es.wikipedia.org/wiki/RAII public: file (const char* filename) : file_ (std::fopen(filename, "w+")) { if (!file_) { throw std::runtime_error ("Error al abrir un archivo."); } } ~file() { if (std::fclose(file_)) { throw std::runtime_error ("Error al cerrar un archivo."); } } void write (const char* str) {...} }
void main () { //////////////////////////////////////////////////////// // Abrimos el archivo, estamos adquiriendo un recurso // //////////////////////////////////////////////////////// file logfile ("registro_de_incidencias.txt"); logfile.write ("¡Hola, registro!"); } // <- Al salir de ámbito, en el destructor de logfile se cierra el fichero
7.1. Patrón RAII y punteros inteligentes.
- El uso del patrón RAII ha dado lugar a la introducción en C++ de los llamados punteros inteligentes o smart pointers.
Ya no será necesario ocuparse de la liberación de memoria reservada dinámicamente, juzga tú mismo:
// compilar con C++11 #include <memory> #include <iostream> int main () { // Compila y ejecuta con valgrind, no // creeras lo que ven tus ojos... std::unique_ptr<int> pe (new int[1024]); // compara con una línea como esta: // int* pe = new int[1024]; return 0; }
7.2. Patrón RAII y excepciones.
- Si echas un vistazo a cómo otros LOO implementan el tratamiento de excepciones, verás que se parece mucho al de C++ (java, C#, Vala, D).
- En casi todos ellos observarás que aparece una palabra reservada
nueva:
finally
. - La introdujo Java al no ser posible implementar de forma correcta el patrón RAII en este lenguaje ya que las variables que representan objetos siempre son referencias y no objetos completos.
Los bloques finally se ejecutan siempre, tanto si se sale del bloque try de forma correcta o por que se ha lanzado una excepción y hemos entrado en un catch.
try { ... } catch (et1) {...} catch (et2) {...} finally { ... }
8. ¿Existe algún otro modo de tratar los errores?
- Sí, claro. Determinados lenguajes de programación han elegido un modelo diferente de tratamiento de errores.
- Veamos el caso de Rust.
En Rust existe este enumerado:
enum Result<T,E> { Ok(T), // Constructor de Result<T> a partir de un tipo T, resultado correcto Err(E), // Constructor de Result<E> a partir de un tipo E, error }
- Podemos ver
Ok(T)
yErr(E)
como sendas funciones que crean un dato de tipoResult
a partir de un dato de tipo:T
si hay resultado,T
es el tipo del resultadoE
si se produce un error,E
es el tipo del error
8.1. Ejemplo de tratamiento de errores en Rust
fn mayor_o_menor_0 (n: i32) -> Result<bool, &'static str> { if n < 0 || n > 0 { Ok(true) } else { Err("n es 0") } } fn main() { let r = mayor_o_menor_0(0); assert!(r.is_err()) }
8.2. Puedes ampliar tus conocimientos sobre este tema consultando este artículo.
9. Aclaraciones
- En ningún caso estas transparencias son la bibliografía de la asignatura, por lo tanto debes estudiar, aclarar y ampliar los conceptos que en ellas encuentres empleando los enlaces web y bibliografía recomendada que puedes consultar en la página web de la ficha de la asignatura y en la web propia de la asignatura.