Eccezioni: throw, try, catch
Comprendi il meccanismo delle eccezioni del C++ — lo srotolamento dello stack, la gerarchia standard delle eccezioni, le eccezioni personalizzate e quando preferire le eccezioni ai codici di errore.
Perché esistono le eccezioni
Prima delle eccezioni, il codice C segnalava gli errori restituendo valori speciali (come -1 o NULL). Questo approccio aveva un difetto critico: chi chiamava poteva ignorare l'errore in silenzio. Le eccezioni risolvono il problema costringendo il programma o a gestire l'errore o a terminare. Separano inoltre il punto di rilevamento dell'errore (in profondità nel codice di libreria) dal punto di gestione dell'errore (nel codice applicativo), che possono trovarsi a molti frame di chiamata di distanza.
Le eccezioni del C++ seguono un modello "a costo zero sul percorso di successo" sulla maggior parte dei compilatori moderni (grazie all'unwinding basato su tabelle). Quando non viene lanciata alcuna eccezione non c'è alcun overhead a runtime — nessun controllo if nascosto, nessuna previsione di salto mancata. Il costo si paga solo quando un'eccezione viene effettivamente lanciata, il che rende le eccezioni ideali per condizioni di errore eccezionali (rare).
throw, try e catch
Il meccanismo di base è semplice: usa throw per lanciare un oggetto che segnala un errore, racchiudi il codice rischioso in un blocco try e usa catch per catturare i tipi di eccezione corrispondenti.
#include <iostream>
#include <stdexcept>
#include <string>
double divide(double numerator, double denominator) {
if (denominator == 0.0) {
throw std::invalid_argument("denominator cannot be zero");
}
return numerator / denominator;
}
int main() {
try {
double result = divide(10.0, 0.0);
std::cout << "Result: " << result << '\n';
} catch (const std::invalid_argument& e) {
std::cerr << "Invalid argument: " << e.what() << '\n';
} catch (const std::exception& e) {
std::cerr << "Error: " << e.what() << '\n';
} catch (...) {
std::cerr << "Unknown error occurred\n";
}
}Lo srotolamento dello stack nel dettaglio
Quando viene lanciata un'eccezione, il runtime esegue lo srotolamento dello stack (stack unwinding): risale la pila delle chiamate distruggendo tutte le variabili locali di ogni frame tramite i rispettivi distruttori. È per questo che il RAII (Resource Acquisition Is Initialization) è così cruciale — se tieni le risorse dentro wrapper RAII, vengono rilasciate automaticamente durante lo srotolamento.
Il processo di srotolamento funziona così:
1. L'espressione throw crea l'oggetto eccezione (di solito in un'area di memoria speciale, non sullo stack).
2. Il runtime cerca nella funzione corrente un gestore catch compatibile.
3. Se non ne trova nessuno, esce dalla funzione corrente distruggendo tutti gli oggetti locali in ordine inverso di costruzione.
4. Ripete i passi 2-3 risalendo la pila delle chiamate.
5. Se non viene trovato alcun gestore in nessun frame, viene chiamata std::terminate().
Regola fondamentale: lancia per valore, cattura per riferimento const. Lanciare per valore evita i puntatori pendenti. Catturare per riferimento const evita lo slicing (quando un'eccezione derivata viene copiata in un oggetto della classe base, perdendo i dati della parte derivata) ed evita copie inutili.
La gerarchia standard delle eccezioni
La libreria standard fornisce una gerarchia che ha come radice std::exception. Conoscerla ti aiuta a lanciare il tipo giusto e a catturare al livello giusto.
// Standard exception hierarchy (simplified)
//
// std::exception
// ├── std::logic_error (programmer mistakes - precondition violations)
// │ ├── std::invalid_argument
// │ ├── std::domain_error
// │ ├── std::length_error
// │ └── std::out_of_range
// ├── std::runtime_error (errors detectable only at runtime)
// │ ├── std::range_error
// │ ├── std::overflow_error
// │ ├── std::underflow_error
// │ └── std::system_error (OS-level errors with error_code)
// └── std::bad_alloc (memory allocation failure)
// std::bad_cast (failed dynamic_cast)
// std::bad_typeid (typeid on null pointer)
#include <stdexcept>
#include <vector>
#include <iostream>
int main() {
std::vector<int> v = {1, 2, 3};
try {
// .at() throws std::out_of_range, unlike operator[] which is UB
int val = v.at(10);
} catch (const std::out_of_range& e) {
std::cerr << "Out of range: " << e.what() << '\n';
}
}Classi di eccezione personalizzate
Deriva da std::exception o da una delle sue sottoclassi per creare eccezioni specifiche del tuo dominio. Fai sempre l'override di what() per fornire un messaggio significativo.
#include <stdexcept>
#include <string>
#include <iostream>
class FileParseError : public std::runtime_error {
public:
FileParseError(const std::string& filename, int line, const std::string& msg)
: std::runtime_error(
"Parse error in '" + filename + "' at line " +
std::to_string(line) + ": " + msg),
filename_(filename),
line_(line) {}
const std::string& filename() const noexcept { return filename_; }
int line() const noexcept { return line_; }
private:
std::string filename_;
int line_;
};
void parse_config(const std::string& filename) {
// Simulate a parse failure at line 42
throw FileParseError(filename, 42, "unexpected token '@@'");
}
int main() {
try {
parse_config("app.conf");
} catch (const FileParseError& e) {
std::cerr << e.what() << '\n';
std::cerr << " File: " << e.filename()
<< ", Line: " << e.line() << '\n';
}
}noexcept e le specifiche di eccezione
Lo specificatore noexcept dice al compilatore (e a chi legge il codice) che una funzione non lancerà mai un'eccezione. Se una funzione noexcept lancia comunque, viene chiamata immediatamente std::terminate() — senza alcuno srotolamento dello stack.
Perché noexcept è importante?
- Prestazioni: il compilatore può omettere le tabelle di unwinding per le funzioni noexcept, producendo codice più compatto.
- Ottimizzazioni della STL: contenitori come std::vector spostano gli elementi durante una riallocazione solo se il costruttore di spostamento è noexcept. Altrimenti ripiegano sulla copia per mantenere la garanzia forte. La differenza di prestazioni può essere enorme.
- Documentazione: è un contratto con chi chiama, che dichiara che la funzione non può fallire.
Usa noexcept su: costruttori di spostamento, operatori di assegnazione per spostamento, funzioni swap, distruttori (implicitamente noexcept), semplici getter e operatori di confronto.
La vecchia specifica throw() (e throw(type-list)) è stata deprecata in C++11 e rimossa in C++20. Usa sempre noexcept al suo posto.
Non lanciare mai eccezioni dai distruttori. Durante lo srotolamento dello stack provocato da un'altra eccezione, lanciarne una seconda comporta la chiamata a std::terminate(). Se un distruttore deve eseguire un'operazione che può fallire, cattura l'eccezione all'interno del distruttore e gestiscila lì (ad esempio registrala nel log e sopprimila).
- Catturare per valore provoca lo slicing delle eccezioni derivate — cattura sempre per
const& - Lanciare puntatori (ad es.
throw new std::runtime_error(...)) provoca memory leak — lancia per valore - Mettere
...per primo nasconde tutti i gestori specifici che lo seguono — mettilo per ultimo - Le eccezioni nei distruttori durante lo srotolamento dello stack causano
std::terminate() - Usare le eccezioni per il flusso di controllo (percorsi che non sono errori) vanifica il modello a costo zero e danneggia le prestazioni
- Lancia per valore e cattura per riferimento
constper evitare slicing e leak - Lo srotolamento dello stack distrugge gli oggetti locali in ordine inverso di costruzione — il RAII garantisce la pulizia
- Le eccezioni del C++ hanno costo zero sul percorso di successo (unwinding basato su tabelle) e costano solo quando vengono lanciate
noexceptabilita ottimizzazioni del compilatore ed è fondamentale per le operazioni di spostamento usate dalla STL- Deriva le eccezioni personalizzate da
std::runtime_errorostd::logic_errore fai l'override diwhat()
Quiz — Metti alla prova le tue conoscenze
(15 XP)1. Perché conviene catturare le eccezioni per `const&` anziché per valore?
2. Cosa succede se una funzione marcata `noexcept` lancia un'eccezione?
3. Durante lo srotolamento dello stack, cosa succede alle variabili locali di ogni frame?