C++
Excepciones
Buscar..
Atrapando excepciones
Se utiliza un bloque try/catch
para capturar excepciones. El código en la sección de try
es el código que puede generar una excepción, y el código en la (s) cláusula (s) de catch
maneja la excepción.
#include <iostream>
#include <string>
#include <stdexcept>
int main() {
std::string str("foo");
try {
str.at(10); // access element, may throw std::out_of_range
} catch (const std::out_of_range& e) {
// what() is inherited from std::exception and contains an explanatory message
std::cout << e.what();
}
}
Se pueden usar múltiples cláusulas catch
para manejar múltiples tipos de excepciones. Si hay varias cláusulas catch
, el mecanismo de manejo de excepciones intenta hacerlas coincidir en el orden en que aparecen en el código:
std::string str("foo");
try {
str.reserve(2); // reserve extra capacity, may throw std::length_error
str.at(10); // access element, may throw std::out_of_range
} catch (const std::length_error& e) {
std::cout << e.what();
} catch (const std::out_of_range& e) {
std::cout << e.what();
}
Las clases de excepción que se derivan de una clase base común se pueden capturar con una única cláusula catch
para la clase base común. El ejemplo anterior puede reemplazar las dos cláusulas catch
para std::length_error
y std::out_of_range
con una sola cláusula para std:exception
:
std::string str("foo");
try {
str.reserve(2); // reserve extra capacity, may throw std::length_error
str.at(10); // access element, may throw std::out_of_range
} catch (const std::exception& e) {
std::cout << e.what();
}
Debido a que las cláusulas de catch
se prueban en orden, asegúrese de escribir primero más cláusulas de captura específicas, de lo contrario, es posible que nunca se llame a su código de control de excepciones:
try {
/* Code throwing exceptions omitted. */
} catch (const std::exception& e) {
/* Handle all exceptions of type std::exception. */
} catch (const std::runtime_error& e) {
/* This block of code will never execute, because std::runtime_error inherits
from std::exception, and all exceptions of type std::exception were already
caught by the previous catch clause. */
}
Otra posibilidad es el controlador catch-all, que capturará cualquier objeto lanzado:
try {
throw 10;
} catch (...) {
std::cout << "caught an exception";
}
Excepción de recirculación (propagación)
A veces, desea hacer algo con la excepción que captura (como escribir en el registro o imprimir una advertencia) y dejar que suba hasta el alcance superior para ser manejado. Para hacerlo, puede volver a lanzar cualquier excepción que detecte:
try {
... // some code here
} catch (const SomeException& e) {
std::cout << "caught an exception";
throw;
}
Utilizando el throw;
sin argumentos volverá a lanzar la excepción capturada actualmente.
Para volver a emitir un std::exception_ptr
administrado, la biblioteca estándar de C ++ tiene la función rethrow_exception
que se puede usar al incluir el encabezado <exception>
en su programa.
#include <iostream>
#include <string>
#include <exception>
#include <stdexcept>
void handle_eptr(std::exception_ptr eptr) // passing by value is ok
{
try {
if (eptr) {
std::rethrow_exception(eptr);
}
} catch(const std::exception& e) {
std::cout << "Caught exception \"" << e.what() << "\"\n";
}
}
int main()
{
std::exception_ptr eptr;
try {
std::string().at(1); // this generates an std::out_of_range
} catch(...) {
eptr = std::current_exception(); // capture
}
handle_eptr(eptr);
} // destructor for std::out_of_range called here, when the eptr is destructed
Función Try Blocks En constructor
La única forma de capturar una excepción en la lista de inicializadores:
struct A : public B
{
A() try : B(), foo(1), bar(2)
{
// constructor body
}
catch (...)
{
// exceptions from the initializer list and constructor are caught here
// if no exception is thrown here
// then the caught exception is re-thrown.
}
private:
Foo foo;
Bar bar;
};
Función Try Block para la función regular
void function_with_try_block()
try
{
// try block body
}
catch (...)
{
// catch block body
}
Que es equivalente a
void function_with_try_block()
{
try
{
// try block body
}
catch (...)
{
// catch block body
}
}
Tenga en cuenta que para los constructores y destructores, el comportamiento es diferente, ya que el bloque catch vuelve a lanzar una excepción de todos modos (el que se atrapa si no hay otro lanzamiento en el cuerpo del bloque catch).
Se permite que la función main
tenga un bloque de prueba de función como cualquier otra función, pero el bloque de prueba de la función main
no detectará las excepciones que se producen durante la construcción de una variable estática no local o la destrucción de cualquier variable estática. En su lugar, se llama std::terminate
.
Función Try Blocks En Destructor
struct A
{
~A() noexcept(false) try
{
// destructor body
}
catch (...)
{
// exceptions of destructor body are caught here
// if no exception is thrown here
// then the caught exception is re-thrown.
}
};
Tenga en cuenta que, aunque esto es posible, hay que tener mucho cuidado al lanzar desde el destructor, ya que si un destructor llamado durante el desenrollado de la pila lanza una excepción, se llama a std::terminate
.
Mejores prácticas: tirar por valor, atrapar por referencia constante
En general, se considera una buena práctica lanzar por valor (en lugar de por puntero), pero capturar por (const) referencia.
try {
// throw new std::runtime_error("Error!"); // Don't do this!
// This creates an exception object
// on the heap and would require you to catch the
// pointer and manage the memory yourself. This can
// cause memory leaks!
throw std::runtime_error("Error!");
} catch (const std::runtime_error& e) {
std::cout << e.what() << std::endl;
}
Una razón por la cual la captura por referencia es una buena práctica es que elimina la necesidad de reconstruir el objeto cuando se pasa al bloque catch (o cuando se propaga a otros bloques catch). La captura por referencia también permite que las excepciones se manejen de forma polimórfica y evita el corte de objetos. Sin embargo, si está volviendo a generar una excepción (como throw e;
vea el ejemplo a continuación), todavía puede obtener el corte de objetos porque la throw e;
La declaración hace una copia de la excepción a medida que se declara el tipo:
#include <iostream>
struct BaseException {
virtual const char* what() const { return "BaseException"; }
};
struct DerivedException : BaseException {
// "virtual" keyword is optional here
virtual const char* what() const { return "DerivedException"; }
};
int main(int argc, char** argv) {
try {
try {
throw DerivedException();
} catch (const BaseException& e) {
std::cout << "First catch block: " << e.what() << std::endl;
// Output ==> First catch block: DerivedException
throw e; // This changes the exception to BaseException
// instead of the original DerivedException!
}
} catch (const BaseException& e) {
std::cout << "Second catch block: " << e.what() << std::endl;
// Output ==> Second catch block: BaseException
}
return 0;
}
Si está seguro de que no va a hacer nada para cambiar la excepción (como agregar información o modificar el mensaje), la captura por referencia constante permite al compilador realizar optimizaciones y mejorar el rendimiento. Pero esto todavía puede causar el empalme de objetos (como se ve en el ejemplo anterior).
Advertencia: tenga cuidado de lanzar excepciones no intencionadas en catch
bloques de catch
, especialmente relacionados con la asignación de memoria o recursos adicionales. Por ejemplo, la construcción de logic_error
, runtime_error
o sus subclases puede bad_alloc
debido a que la memoria se está agotando al copiar la cadena de excepción, las secuencias de E / S pueden lanzarse durante el registro con el conjunto de máscaras de excepción respectivas, etc.
La excepción jerarquizada
Durante el manejo de excepciones, hay un caso de uso común cuando detecta una excepción genérica de una función de bajo nivel (como un error del sistema de archivos o un error de transferencia de datos) y lanza una excepción de alto nivel más específica que indica que alguna operación de alto nivel podría no se realiza (como no poder publicar una foto en la Web). Esto permite que el manejo de excepciones reaccione a problemas específicos con operaciones de alto nivel y también permite, al tener solo un mensaje de error, al programador encontrar un lugar en la aplicación donde se produjo una excepción. La desventaja de esta solución es que la excepción callstack se trunca y la excepción original se pierde. Esto obliga a los desarrolladores a incluir manualmente el texto de la excepción original en uno recién creado.
Las excepciones anidadas intentan resolver el problema adjuntando la excepción de bajo nivel, que describe la causa, a una excepción de alto nivel, que describe lo que significa en este caso particular.
std::nested_exception
permite anidar excepciones gracias a std::throw_with_nested
:
#include <stdexcept>
#include <exception>
#include <string>
#include <fstream>
#include <iostream>
struct MyException
{
MyException(const std::string& message) : message(message) {}
std::string message;
};
void print_current_exception(int level)
{
try {
throw;
} catch (const std::exception& e) {
std::cerr << std::string(level, ' ') << "exception: " << e.what() << '\n';
} catch (const MyException& e) {
std::cerr << std::string(level, ' ') << "MyException: " << e.message << '\n';
} catch (...) {
std::cerr << "Unkown exception\n";
}
}
void print_current_exception_with_nested(int level = 0)
{
try {
throw;
} catch (...) {
print_current_exception(level);
}
try {
throw;
} catch (const std::nested_exception& nested) {
try {
nested.rethrow_nested();
} catch (...) {
print_current_exception_with_nested(level + 1); // recursion
}
} catch (...) {
//Empty // End recursion
}
}
// sample function that catches an exception and wraps it in a nested exception
void open_file(const std::string& s)
{
try {
std::ifstream file(s);
file.exceptions(std::ios_base::failbit);
} catch(...) {
std::throw_with_nested(MyException{"Couldn't open " + s});
}
}
// sample function that catches an exception and wraps it in a nested exception
void run()
{
try {
open_file("nonexistent.file");
} catch(...) {
std::throw_with_nested( std::runtime_error("run() failed") );
}
}
// runs the sample function above and prints the caught exception
int main()
{
try {
run();
} catch(...) {
print_current_exception_with_nested();
}
}
Salida posible:
exception: run() failed
MyException: Couldn't open nonexistent.file
exception: basic_ios::clear
Si trabaja solo con excepciones heredadas de std::exception
, el código puede incluso simplificarse.
std :: uncaught_exceptions
C ++ 17 introduce int std::uncaught_exceptions()
(para reemplazar la limitada bool std::uncaught_exception()
) para saber cuántas excepciones actualmente no se detectan. Eso permite que una clase determine si se destruye durante el desenrollado de una pila o no.
#include <exception>
#include <string>
#include <iostream>
// Apply change on destruction:
// Rollback in case of exception (failure)
// Else Commit (success)
class Transaction
{
public:
Transaction(const std::string& s) : message(s) {}
Transaction(const Transaction&) = delete;
Transaction& operator =(const Transaction&) = delete;
void Commit() { std::cout << message << ": Commit\n"; }
void RollBack() noexcept(true) { std::cout << message << ": Rollback\n"; }
// ...
~Transaction() {
if (uncaughtExceptionCount == std::uncaught_exceptions()) {
Commit(); // May throw.
} else { // current stack unwinding
RollBack();
}
}
private:
std::string message;
int uncaughtExceptionCount = std::uncaught_exceptions();
};
class Foo
{
public:
~Foo() {
try {
Transaction transaction("In ~Foo"); // Commit,
// even if there is an uncaught exception
//...
} catch (const std::exception& e) {
std::cerr << "exception/~Foo:" << e.what() << std::endl;
}
}
};
int main()
{
try {
Transaction transaction("In main"); // RollBack
Foo foo; // ~Foo commit its transaction.
//...
throw std::runtime_error("Error");
} catch (const std::exception& e) {
std::cerr << "exception/main:" << e.what() << std::endl;
}
}
Salida:
In ~Foo: Commit
In main: Rollback
exception/main:Error
Excepción personalizada
No debe lanzar valores brutos como excepciones, en su lugar, utilice una de las clases de excepción estándar o cree las suyas propias.
Tener su propia clase de excepción heredada de std::exception
es una buena manera de hacerlo. Aquí hay una clase de excepción personalizada que hereda directamente de std::exception
:
#include <exception>
class Except: virtual public std::exception {
protected:
int error_number; ///< Error number
int error_offset; ///< Error offset
std::string error_message; ///< Error message
public:
/** Constructor (C++ STL string, int, int).
* @param msg The error message
* @param err_num Error number
* @param err_off Error offset
*/
explicit
Except(const std::string& msg, int err_num, int err_off):
error_number(err_num),
error_offset(err_off),
error_message(msg)
{}
/** Destructor.
* Virtual to allow for subclassing.
*/
virtual ~Except() throw () {}
/** Returns a pointer to the (constant) error description.
* @return A pointer to a const char*. The underlying memory
* is in possession of the Except object. Callers must
* not attempt to free the memory.
*/
virtual const char* what() const throw () {
return error_message.c_str();
}
/** Returns error number.
* @return #error_number
*/
virtual int getErrorNumber() const throw() {
return error_number;
}
/**Returns error offset.
* @return #error_offset
*/
virtual int getErrorOffset() const throw() {
return error_offset;
}
};
Un ejemplo de lanzar catch:
try {
throw(Except("Couldn't do what you were expecting", -12, -34));
} catch (const Except& e) {
std::cout<<e.what()
<<"\nError number: "<<e.getErrorNumber()
<<"\nError offset: "<<e.getErrorOffset();
}
Como no solo está lanzando un mensaje de error simple, sino también algunos otros valores que representan el error exactamente, su manejo de errores se vuelve mucho más eficiente y significativo.
Hay una clase de excepción que te permite manejar bien los mensajes de error: std::runtime_error
También puedes heredar de esta clase:
#include <stdexcept>
class Except: virtual public std::runtime_error {
protected:
int error_number; ///< Error number
int error_offset; ///< Error offset
public:
/** Constructor (C++ STL string, int, int).
* @param msg The error message
* @param err_num Error number
* @param err_off Error offset
*/
explicit
Except(const std::string& msg, int err_num, int err_off):
std::runtime_error(msg)
{
error_number = err_num;
error_offset = err_off;
}
/** Destructor.
* Virtual to allow for subclassing.
*/
virtual ~Except() throw () {}
/** Returns error number.
* @return #error_number
*/
virtual int getErrorNumber() const throw() {
return error_number;
}
/**Returns error offset.
* @return #error_offset
*/
virtual int getErrorOffset() const throw() {
return error_offset;
}
};
Tenga en cuenta que no he anulado la función what()
de la clase base ( std::runtime_error
), es decir, std::runtime_error
la versión de la clase base de what()
. Puedes anularlo si tienes otra agenda.