Gestione delle eccezioni in C++
Nei programmi reali è inevitabile che si verifichino situazioni anomale: un file che non si apre, una memoria che non si alloca, un dato in input non valido. La gestione degli errori serve a far fronte a queste circostanze in modo controllato, evitando che il programma termini bruscamente o produca risultati scorretti. Il C++ mette a disposizione un meccanismo strutturato per questo scopo: le eccezioni, che separano nettamente il codice che svolge il lavoro da quello che gestisce gli errori.
Il meccanismo delle eccezioni
Quando si verifica una condizione di errore, il codice può lanciare un'eccezione con la parola chiave throw. Il lancio interrompe immediatamente il flusso normale e trasferisce il controllo al gestore appropriato. Il codice che potrebbe lanciare un'eccezione viene racchiuso in un blocco try, mentre il gestore dell'errore si trova in un blocco catch che lo segue. Se durante l'esecuzione del blocco try viene lanciata un'eccezione, il controllo passa al blocco catch compatibile.
#include <iostream>
#include <stdexcept>
double divide(double numerator, double denominator) {
if (denominator == 0.0) {
// Segnala l'errore lanciando un'eccezione
throw std::invalid_argument("Divisione per zero");
}
return numerator / denominator;
}
int main() {
try {
double result = divide(10.0, 0.0);
std::cout << result << std::endl;
} catch (const std::invalid_argument& error) {
// Gestisce l'errore in modo controllato
std::cout << "Errore: " << error.what() << std::endl;
}
return 0;
}
Il metodo what restituisce una descrizione testuale dell'errore. Catturando l'eccezione tramite un riferimento costante si evita una copia inutile e si gestisce correttamente l'intera gerarchia delle eccezioni.
La gerarchia delle eccezioni standard
La libreria standard definisce, nell'intestazione stdexcept, una gerarchia di classi di eccezione tutte derivate da std::exception. Tra le più usate vi sono std::invalid_argument per argomenti non validi, std::out_of_range per accessi fuori dai limiti e std::runtime_error per errori che emergono durante l'esecuzione. Poiché derivano tutte da una base comune, è possibile catturarle in modo generico attraverso un riferimento alla classe base.
try {
// Codice che potrebbe lanciare diverse eccezioni
} catch (const std::exception& error) {
// Cattura qualsiasi eccezione standard grazie al polimorfismo
std::cout << error.what() << std::endl;
}
Gestori multipli
Un blocco try può essere seguito da più blocchi catch, ciascuno specializzato per un tipo di eccezione. Vengono valutati nell'ordine in cui sono scritti, e viene eseguito il primo compatibile con l'eccezione lanciata. È buona prassi disporre i gestori dal più specifico al più generale, poiché un gestore per la classe base catturerebbe anche le eccezioni derivate, rendendo irraggiungibili i gestori più specifici scritti dopo di esso.
try {
// Operazione rischiosa
} catch (const std::out_of_range& error) {
std::cout << "Indice fuori intervallo" << std::endl;
} catch (const std::exception& error) {
std::cout << "Errore generico" << std::endl;
}
Lo svolgimento dello stack e RAII
Quando un'eccezione viene lanciata, il programma risale la catena delle chiamate alla ricerca di un gestore, distruggendo lungo il percorso tutti gli oggetti locali creati nei blocchi attraversati. Questo processo è detto svolgimento dello stack. È qui che il principio RAII mostra tutta la sua importanza: poiché i distruttori vengono invocati automaticamente durante lo svolgimento, le risorse gestite da oggetti RAII, come gli smart pointer, vengono rilasciate correttamente anche in presenza di eccezioni. Al contrario, la memoria allocata manualmente con new rischia di non essere mai liberata se un'eccezione interrompe il flusso prima del delete.
Quando usare le eccezioni
Le eccezioni sono indicate per gli errori veri e propri, ovvero condizioni eccezionali che impediscono il normale proseguimento dell'operazione. Non andrebbero usate per il controllo del flusso ordinario né per situazioni previste e frequenti, perché il loro lancio comporta un costo. La regola generale è lanciare un'eccezione quando una funzione non può adempiere al proprio compito e non ha modo di gestire localmente il problema, lasciando la decisione su come reagire al codice chiamante. Una gestione degli errori ben progettata, fondata sulle eccezioni e sul principio RAII, rende i programmi robusti e prevedibili anche di fronte a circostanze impreviste.