Perché esistono le eccezioni
Nella pagina precedente hai usato enum class per dare nomi significativi agli stati di errore. È ottimo per gli esiti che una funzione si aspetta e che il chiamante deve esaminare. Ma alcuni fallimenti sono diversi: una funzione in profondità nello stack di chiamate scopre che un file non si apre, o che un argomento non ha senso, e non ha idea di cosa dovrebbe fare il programma a riguardo. Restituire un codice di errore funziona solo se ogni chiamante della catena si ricorda di controllarlo e di passarlo più su. Basta saltare un controllo e il programma va avanti con dati spazzatura.
Le eccezioni risolvono questo problema. Quando qualcosa va storto, lanci un oggetto con throw. L'esecuzione si ferma subito, lo stack viene srotolato (per ogni oggetto locale tra il throw e il gestore viene eseguito il distruttore) e il controllo salta al catch corrispondente più vicino. Un'eccezione non gestita non può essere ignorata in silenzio: se nulla la intercetta, il programma chiama std::terminate e si interrompe.
Questa pagina si concentra sul lato del lancio, cioè sugli oggetti di errore in sé. La prossima pagina approfondisce nel dettaglio il meccanismo di try/catch.
Lanciare eccezioni e il messaggio di what()
Tecnicamente puoi lanciare con throw qualsiasi valore (throw 42; o throw "oops"; sono legali), ma non farlo. La convenzione seguita da tutti è lanciare un oggetto derivato da std::exception. Quella classe base dichiara un metodo virtuale, what(), che restituisce una descrizione del problema come const char*. Rispettare la convenzione significa che un unico catch (const std::exception& e) può gestire qualsiasi cosa.
L'header <stdexcept> ti offre tipi pronti all'uso il cui costruttore riceve il messaggio:
Nota che what() restituisce esattamente la stringa con cui hai costruito l'eccezione. Nota anche che abbiamo intercettato con const exception& anche se avevamo lanciato un runtime_error: funziona perché runtime_error è un std::exception (una relazione che riconoscerai dalla pagina sull'ereditarietà).
La gerarchia delle eccezioni standard
Prima di scrivere un tuo tipo di eccezione, controlla se la libreria standard ne ha già uno adatto. Ereditano tutti da std::exception e in <stdexcept> si dividono in due famiglie:
logic_error: un bug nella logica del programma che, in linea di principio, si potrebbe individuare prima dell'esecuzione. I sottotipi includonoinvalid_argument,out_of_range,domain_errorelength_error.runtime_error: un fallimento che si manifesta solo a runtime e che di per sé non è un errore di programmazione. I sottotipi includonorange_error,overflow_erroreunderflow_error.
Molte funzioni di libreria le lanciano al posto tuo. Per esempio, at() del contenitore vector controlla i limiti e lancia out_of_range invece di lasciarti leggere oltre la fine:
Quell'at() è la controparte sicura di v[9]. Il semplice operator[] non controlla i limiti: leggere v[9] qui è comportamento indefinito, non un'eccezione. Scegliere at() è il modo in cui trasformi una corruzione silenziosa in un errore intercettabile.
Scegli il tipo che descrive l'errore: invalid_argument quando un chiamante passa qualcosa di insensato, out_of_range per problemi di indici o chiavi, runtime_error per "il mondo esterno mi ha tradito".
Scrivere un tuo tipo di eccezione
Quando nessun tipo standard è adatto, perché vuoi allegare dati in più o intercettare con catch solo il tuo errore e nient'altro, definisci una classe che eredita da std::exception (o da uno dei suoi sottotipi) e ridefinisci what(). Ereditare da std::runtime_error è la strada più semplice, perché memorizza già il messaggio e implementa what() per te:
Dato che NetworkError porta con sé un codice di stato, il gestore può reagire di conseguenza: riprovare con un 5xx, arrendersi con un 4xx. Una semplice stringa di errore non lo permetterebbe. Il tipo personalizzato consente anche a un catch (const NetworkError&) di intercettare solo i problemi di rete, lasciando tutto il resto al gestore più generico che sta sotto.
Se erediti direttamente da std::exception (e non da runtime_error), ricordati di ridefinire tu what() e di marcarlo noexcept per rispettare la firma della classe base:
class ParseError : public std::exception {
public:
const char* what() const noexcept override {
return "failed to parse input";
}
};
Lancia per valore, intercetta per riferimento
È la regola più importante delle eccezioni in C++, ed è quella che i principianti sbagliano. Lancia gli oggetti per valore e intercettali per riferimento const.
throw runtime_error("oops"); // per valore, corretto
catch (const runtime_error& e) { ... } // per riferimento const, corretto
Intercettare invece per valore, con catch (std::exception e), copia l'eccezione in un oggetto della classe base e ne taglia via la parte derivata (object slicing). Dopo il taglio, e.what() chiama l'implementazione della base, non quella che hai ridefinito, e il tuo messaggio scritto con cura sparisce:
try {
throw NetworkError(503, "service unavailable");
} catch (std::exception e) { // per valore, object slicing!
std::cout << e.what(); // messaggio generico, status() è sparito
}
Il riferimento (&) preserva il vero tipo dinamico, quindi il what() virtuale viene risolto correttamente e puoi ancora accedere ai membri della classe derivata. Aggiungi const perché stai solo leggendo l'eccezione, non modificandola. Non lanciare mai un puntatore (throw new runtime_error(...)): chi lo intercetta dovrebbe farne il delete, ma in quale percorso del codice? È esattamente il leak che le eccezioni dovrebbero prevenire.
Prossimo: try-catch
Ora sai creare e lanciare con throw eccezioni ben fatte e scegliere il tipo standard giusto per ogni fallimento. L'altra metà della storia è il lato dell'intercettazione. La prossima pagina tratta try/catch per intero: come ordinare più blocchi catch dal più specifico al più generico, il catch (...) che intercetta tutto, come rilanciare con un semplice throw; e come RAII (ripensa agli smart pointer) garantisce che le tue risorse vengano rilasciate mentre lo stack viene srotolato.
Domande frequenti
Cos'è un'eccezione in C++?
Un'eccezione è un oggetto che segnala un errore che la funzione corrente non può gestire da sola. La lanci con throw, lo stack viene srotolato (distruggendo gli oggetti locali lungo il percorso) e un blocco catch corrispondente più in alto prende il controllo. Separa il codice che rileva un problema da quello che decide cosa fare.
Qual è la differenza tra throw e return per gli errori?
Un valore restituito con return deve essere controllato dal chiamante, ed è facile dimenticarsene: il programma prosegue semplicemente con dati sbagliati. Un'eccezione lanciata non si può ignorare: se nessuno la intercetta, il programma termina. Le eccezioni servono per i veri fallimenti (un file che non si apre, un input non valido); i valori di ritorno restano la scelta giusta per i risultati ordinari, compresi i casi previsti di "non trovato".
A cosa serve il metodo what() nelle eccezioni C++?
Ogni classe derivata da std::exception fornisce un metodo virtuale what() che restituisce un const char* che descrive l'errore. Quando intercetti un'eccezione, chiamare e.what() ti dà il messaggio leggibile da registrare nei log o stampare. I tipi di eccezione standard lo impostano a partire dalla stringa che passi al loro costruttore.