Ricerca…
Cattura eccezioni
Un blocco try/catch
viene utilizzato per rilevare le eccezioni. Il codice nella sezione try
è il codice che può generare un'eccezione e il codice nella clausola catch
gestisce l'eccezione.
#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();
}
}
È possibile utilizzare più clausole di catch
per gestire più tipi di eccezioni. Se sono presenti più clausole catch
, il meccanismo di gestione delle eccezioni tenta di farli corrispondere in ordine di aspetto nel codice:
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();
}
Le classi di eccezioni derivate da una classe base comune possono essere catturate con una singola clausola catch
per la classe base comune. L'esempio sopra può sostituire le due clausole catch
per std::length_error
e std::out_of_range
con una singola clausola per 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();
}
Poiché le clausole di catch
vengono provate in ordine, assicurarsi di scrivere prima clausole di cattura più specifiche, altrimenti il codice di gestione delle eccezioni potrebbe non essere mai chiamato:
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. */
}
Un'altra possibilità è il gestore catch-all, che catturerà qualsiasi oggetto lanciato:
try {
throw 10;
} catch (...) {
std::cout << "caught an exception";
}
Rethrow (propagare) l'eccezione
A volte si vuole fare qualcosa con l'eccezione che si cattura (come scrivere per registrare o stampare un avviso) e lasciarlo gonfiare verso l'ambito superiore per essere gestito. Per fare ciò, puoi ripensare a qualsiasi eccezione che ricevi:
try {
... // some code here
} catch (const SomeException& e) {
std::cout << "caught an exception";
throw;
}
Usando il throw;
senza argomenti restituirà l'eccezione attualmente rilevata.
Per rilanciare uno std::exception_ptr
gestito, la libreria standard C ++ ha la funzione rethrow_exception
che può essere utilizzata includendo l'intestazione <exception>
nel programma.
#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
Funzione Try Blocks In constructor
L'unico modo per rilevare l'eccezione nell'elenco di inizializzazione:
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;
};
Funzione Prova blocco per funzione normale
void function_with_try_block()
try
{
// try block body
}
catch (...)
{
// catch block body
}
Che è equivalente a
void function_with_try_block()
{
try
{
// try block body
}
catch (...)
{
// catch block body
}
}
Si noti che per costruttori e distruttori, il comportamento è diverso in quanto il blocco catch re-getta comunque un'eccezione (quella catturata se non c'è nessun altro lancio nel corpo del blocco catch).
La funzione main
può avere una funzione try block come qualsiasi altra funzione, ma il blocco try della funzione main
non rileverà eccezioni che si verificano durante la costruzione di una variabile statica non locale o la distruzione di qualsiasi variabile statica. Invece, std::terminate
viene chiamato.
Funzione Try Blocks In 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.
}
};
Nota che, sebbene sia possibile, bisogna stare molto attenti con il lancio dal distruttore, come se un distruttore chiamato durante lo sbobinamento dello stack lanci un'eccezione, std::terminate
viene chiamato.
Best practice: lancio per valore, cattura per riferimento const
In generale, è considerata una buona pratica lanciare per valore (piuttosto che per puntatore), ma catturare per riferimento (const).
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 delle ragioni per cui la cattura per riferimento è una buona pratica è che elimina la necessità di ricostruire l'oggetto quando viene passato al blocco catch (o quando si propagano attraverso altri blocchi catch). La cattura per riferimento consente inoltre di gestire le eccezioni in modo polimorfico ed evitare l'affettamento degli oggetti. Tuttavia, se stai rilanciando un'eccezione (come throw e;
vedi l'esempio sotto), puoi ancora ottenere l'affettamento degli oggetti perché il throw e;
l'istruzione fa una copia dell'eccezione come qualunque sia il tipo dichiarato:
#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;
}
Se sei sicuro di non fare nulla per modificare l'eccezione (come aggiungere informazioni o modificare il messaggio), l'acquisizione tramite riferimento const consente al compilatore di effettuare ottimizzazioni e migliorare le prestazioni. Ma questo può ancora causare lo splicing dell'oggetto (come mostrato nell'esempio sopra).
Avviso: fare attenzione a non generare eccezioni involontarie nei blocchi catch
, in particolare in relazione all'allocazione di memoria o risorse aggiuntive. Ad esempio, la costruzione di logic_error
, runtime_error
o delle loro sottoclassi potrebbe generare bad_alloc
causa della memoria che si esaurisce durante la copia della stringa di eccezione, i flussi di I / O potrebbero generare durante la registrazione con rispettive maschere di eccezione impostate, ecc.
Eccezione annidata
Durante la gestione delle eccezioni esiste un caso d'uso comune quando si cattura un'eccezione generica da una funzione di basso livello (come un errore del filesystem o di trasferimento dei dati) e si genera un'eccezione più specifica di alto livello che indica che alcune operazioni di alto livello potrebbero non essere eseguito (come non essere in grado di pubblicare una foto sul Web). Ciò consente alla gestione delle eccezioni di reagire a problemi specifici con operazioni di alto livello e consente inoltre, avendo solo un messaggio di errore, che il programmatore trovi un posto nell'applicazione in cui si è verificata un'eccezione. Il lato negativo di questa soluzione è che l'eccezione del callstack viene troncata e l'eccezione originale viene persa. Ciò impone agli sviluppatori di includere manualmente il testo dell'eccezione originale in uno appena creato.
Le eccezioni nidificate mirano a risolvere il problema allegando un'eccezione di basso livello, che descrive la causa, a un'eccezione di alto livello, che descrive cosa significa in questo caso particolare.
std::nested_exception
consente di nidificare le eccezioni grazie 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();
}
}
Uscita possibile:
exception: run() failed
MyException: Couldn't open nonexistent.file
exception: basic_ios::clear
Se lavori solo con le eccezioni ereditate da std::exception
, il codice può anche essere semplificato.
std :: uncaught_exceptions
C ++ 17 introduce int std::uncaught_exceptions()
(per sostituire il bool std::uncaught_exception()
limitato bool std::uncaught_exception()
) per sapere quante eccezioni sono attualmente non rilevate. Ciò consente a una classe di determinare se viene distrutta durante lo svolgimento di una pila o meno.
#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;
}
}
Produzione:
In ~Foo: Commit
In main: Rollback
exception/main:Error
Eccezione personalizzata
Non dovresti lanciare valori grezzi come eccezioni, usa invece una delle classi di eccezioni standard o creane una tua.
Avere la propria classe di eccezione ereditata da std::exception
è un buon modo per farlo. Ecco una classe di eccezioni personalizzata che eredita direttamente da 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 esempio di presa del tiro:
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();
}
Dato che non stai solo lanciando un messaggio di errore stupido, anche altri valori che rappresentano esattamente l'errore, la gestione degli errori diventa molto più efficiente e significativa.
Esiste una classe di eccezioni che consente di gestire bene i messaggi di errore: std::runtime_error
Puoi ereditare anche da questa classe:
#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;
}
};
Nota che non ho sovrascritto la funzione what()
dalla classe base ( std::runtime_error
), cioè useremo la versione della classe base di what()
. Puoi sovrascriverlo se hai ulteriori impegni.