1. Perché serve std::exception, se si può lanciare di tutto
Se guardi C++ in modo strettamente formale, si può lanciare quasi qualsiasi cosa: un numero, una stringa, una struct, persino un gatto (se il gatto è copiabile). Il problema è che gestire “qualsiasi cosa” è scomodo: non sai come stamparlo, come classificarlo e come scrivere un gestore comune.
std::exception risolve proprio questo problema pratico: fornisce un'interfaccia minima comune “sono un errore, ecco il mio testo”.
std::exception è la classe base delle eccezioni standard. La cosa principale che introduce nel nostro codice è il metodo:
const char* what() const noexcept;
Quindi un'eccezione (se compatibile con std::exception) sa fornire una descrizione testuale del problema. Non perfetta, non sempre dettagliata, ma almeno qualcosa, invece del generico “è successo un errore”.
È anche importante e pratico questo fatto: la stragrande maggioranza delle eccezioni della libreria standard deriva da std::exception, perciò l’handler “universale” di solito appare come catch (const std::exception& e).
Una piccola schema per fissare il ruolo di std::exception:
flowchart TD
A["throw (un oggetto errore qualunque)"] --> B{Cerchiamo un catch adatto}
B -->|"catch (const SomeType&)"| C[Gestione specifica]
B -->|"catch (const std::exception& e)"| D["Handler generale
si può stampare e.what()"]
B -->|"catch (...)"| E["Piano B
tipo sconosciuto, no what()"]
2. Regole di base: come catturare le eccezioni
Prendiamo std::exception per riferimento con const&
Quando l'eccezione è risalita dal profondo delle tue funzioni e arriva in catch, di solito vuoi due cose: prima di tutto non perdere le informazioni sull'errore e, in secondo luogo, evitare copie inutili.
Perciò la “formula d’oro” per il caso generale è: catturare per riferimento const. È una pratica consolidata: le eccezioni polimorfiche si catturano per riferimento per non perdere il tipo e il comportamento dinamico.
Ecco lo scheletro minimo di “catturato e stampato”:
#include <exception>
#include <iostream>
#include <stdexcept>
int main() {
try {
throw std::runtime_error("something went wrong");
} catch (const std::exception& e) {
std::cout << "error: " << e.what() << '\n'; // error: something went wrong
}
}
Nota: per ora non discutiamo “quale tipo di eccezione scegliere” — sarà nella lezione successiva. Qui ci interessa l'interfaccia comune: se l'eccezione deriva da std::exception, allora ha what().
Perché const? Perché nel gestore di solito non dovresti “modificare” l'eccezione. Devi leggerla, stampare il messaggio e magari terminare l'operazione. E il & (riferimento) evita di fare copie e di perdere il tipo dinamico (di questo parleremo a breve).
Ordine dei catch: dove mettere std::exception e dove — catch (...)
Quando il codice ha più handler, il compilatore sceglie il primo adatto per tipo. Quindi l'ordine dei catch non è “estetica”, ma parte della logica.
Nella pratica std::exception è una “rete ampia”: cattura molto. Se la metti per prima, tutti gli handler più specifici sotto diventeranno irraggiungibili.
Mantieni nella testa la scala corretta:
- Prima — i tipi più concreti (se prevedi di gestirli in modo specifico).
- Poi — l'handler generale const std::exception&.
- E solo dopo — catch (...), come ultima linea di difesa quando il tipo è totalmente sconosciuto.
Esempio minimale, solo per vedere la struttura (senza approfondire i tipi — lo faremo dopo):
#include <exception>
#include <iostream>
#include <stdexcept>
int main() {
try {
throw std::runtime_error("boom");
} catch (const std::runtime_error& e) {
std::cout << "runtime_error: " << e.what() << '\n'; // runtime_error: boom
} catch (const std::exception& e) {
std::cout << "std::exception: " << e.what() << '\n';
} catch (...) {
std::cout << "unknown error\n";
}
}
Nota: catch (...) non ti dà what(), perché lì non hai un oggetto eccezione tipizzato. È “catturo tutto, ma non so nulla”. Perciò catch (...) non sostituisce la diagnostica normale, ma è una rete di sicurezza per evitare che il programma termini in modo silenzioso.
3. what(): cosa restituisce e come stamparlo
Quando vedi per la prima volta what(), spesso ti aspetti una std::string: “ma è testo”. Storicamente (e per compatibilità) what() restituisce un const char*. Questo significa: hai in mano un puntatore a una C-string, cioè una stringa terminata da '\0'.
Lo scenario più semplice (e corretto) è stamparlo direttamente dentro il catch:
#include <exception>
#include <iostream>
#include <stdexcept>
int main() {
try {
throw std::runtime_error("division by zero");
} catch (const std::exception& e) {
std::cout << e.what() << '\n'; // division by zero
}
}
Perché è comodo? Perché ottieni almeno una stringa nei log/console invece del criptico “programma terminato”.
Un po' sul lifetime
C'è una sottigliezza: what() restituisce un puntatore a memoria che appartiene all'oggetto eccezione. Finché sei dentro il catch, l'oggetto eccezione è vivo e la stringa è in genere valida. Ma appena esci dal catch, conservare quel puntatore “per dopo” è una pessima idea.
Esempio sbagliato (non farlo):
#include <exception>
#include <iostream>
#include <stdexcept>
int main() {
const char* msg = nullptr;
try {
throw std::runtime_error("bad input");
} catch (const std::exception& e) {
msg = e.what();
}
std::cout << msg << '\n'; // ⚠ potrebbe non essere più valido (dangling pointer)
}
La soluzione buona e “umana” è copiare il testo in una std::string se ti serve il messaggio dopo il catch:
#include <exception>
#include <iostream>
#include <stdexcept>
#include <string>
int main() {
std::string msg;
try {
throw std::runtime_error("bad input");
} catch (const std::exception& e) {
msg = e.what(); // copio il testo
}
std::cout << msg << '\n'; // bad input
}
Ricorda la metafora: what() è una “nota” attaccata all'oggetto eccezione. Finché l'oggetto è nelle tue mani — la nota si può leggere. Se vuoi portarti via la nota — riscrivila sul tuo taccuino (std::string).
4. Perché non si deve catturare std::exception per valore
Ora arriva un punto che ti confonde una volta sola — e poi ti risparmia ore di debug.
Le eccezioni sono spesso polimorfiche: lanci un oggetto di un tipo derivato, per esempio un runtime error specifico, e lo catturi con il tipo base std::exception. Questo è previsto.
Ma se catturi per valore:
catch (std::exception e) { ... }
avviene lo slicing dell'oggetto (object slicing): la parte derivata viene “tagliata” e nella variabile e rimane solo la parte base std::exception. Di conseguenza potresti perdere l'implementazione “corretta” di what() e ottenere un testo poco utile.
Confrontiamo i due casi.
Brutta pratica — catturi per valore:
#include <exception>
#include <iostream>
#include <stdexcept>
int main() {
try {
throw std::runtime_error("file not found");
} catch (std::exception e) { // ⚠ catturo per valore
std::cout << e.what() << '\n'; // spesso sarà qualcosa come "std::exception"
}
}
Buona pratica — catturi per riferimento:
#include <exception>
#include <iostream>
#include <stdexcept>
int main() {
try {
throw std::runtime_error("file not found");
} catch (const std::exception& e) { // ✅ per riferimento
std::cout << e.what() << '\n'; // file not found
}
}
Ecco perché la regola “cattura con const&” non è un dogma estetico ma un modo per non perdere il messaggio d'errore.
Pensa che catturare per valore è come ricevere una scatola con l'etichetta “Fragile glass”, rispiegarla in una scatola “Scatola qualunque” e stupirti che non ci sia la scritta “non gettare”.
5. Esempio pratico: mini-calcolatrice e diagnostica tramite what()
Costruiamo una piccola calcolatrice da console “povera ma orgogliosa”. Leggerà righe del tipo 12 / 3 e stamperà il risultato. Se qualcosa va storto — invece di terminare in silenzio mostrerà il messaggio d'errore tramite what().
Step 1: funzione di divisione che controlla lo zero
Cominciamo con una piccola funzione. Se dividi per zero non vogliamo restituire un “valore magico” tipo 0 e fingere che tutto sia OK. Vogliamo segnalare l'errore.
#include <stdexcept>
double safe_div(double a, double b) {
if (b == 0.0) {
throw std::runtime_error("division by zero");
}
return a / b;
}
Sì, qui usiamo std::runtime_error. Oggi non ci interessa “perché proprio quello”, ma il fatto che può essere catturato come std::exception e che possiamo leggere what().
Step 2: valutazione dell'espressione in base all'operatore
Aggiungiamo un semplice dispatcher per le operazioni. Niente di magico, solo quattro operatori.
#include <stdexcept>
double eval(double a, char op, double b) {
if (op == '+') return a + b;
if (op == '-') return a - b;
if (op == '*') return a * b;
if (op == '/') return safe_div(a, b);
throw std::runtime_error("unknown operator");
}
Nota lo stile: se l'operatore è sconosciuto non proviamo a “indovinarlo”. Interrompiamo l'operazione con un'eccezione.
Step 3: parsing della riga con std::stringstream
Ora leggiamo la riga e otteniamo a op b. Abbiamo già visto il parsing con i flussi, quindi qui non c'è niente di speciale.
#include <sstream>
#include <stdexcept>
#include <string>
struct Expr {
double a{};
char op{};
double b{};
};
Expr parse_expr(const std::string& line) {
std::stringstream ss(line);
Expr e{};
if (!(ss >> e.a >> e.op >> e.b)) {
throw std::runtime_error("bad expression format");
}
return e;
}
Se la riga è vuota o malformata lanciamo un'eccezione. Chi decide cosa farne è chi governa l'I/O: di solito main() o una funzione di livello “run”.
Step 4: confine della gestione — try/catch in main()
Ora assemblamo il main “sottile” che sa: leggere una riga, parse, calcolare, stampare. E, cosa importante, in un solo punto catturare le eccezioni come std::exception e stampare what().
#include <exception>
#include <iostream>
#include <string>
int main() {
try {
std::string line;
std::getline(std::cin, line);
const Expr ex = parse_expr(line);
const double result = eval(ex.a, ex.op, ex.b);
std::cout << result << '\n'; // ad esempio: 4
} catch (const std::exception& e) {
std::cout << "error: " << e.what() << '\n';
}
}
Cosa ci siamo guadagnati? Se l'input era 12 / 0, il programma non muore in silenzio né stampa “0”. Dirà onestamente error: division by zero. Se l'input era hello, stamperà error: bad expression format.
Soprattutto — questo stile è scalabile: puoi aggiungere funzionalità e continuare ad avere un handler comune che non trasforma il debug in divinazione.
Confini di responsabilità
Osserva come si sono distribuiti i ruoli. Le funzioni parse_expr e eval non stampano errori né domandano all'utente “sei sicuro?”. Si limitano a segnalare: “non posso svolgere il compito”. Il main() decide cosa fare con questa notizia: mostrare il messaggio e terminare (o, in applicazioni più complesse, richiedere nuovamente l'input).
È un buon design anche a livello principiante: meno confusione da std::cout sparsi in tutto il progetto.
6. Errori tipici lavorando con std::exception e what()
Quando inizi a usare le eccezioni in codice reale, gli errori più frequenti non sono nella teoria complicata, ma nelle abitudini quotidiane. Molti sembrano innocui perché il programma a volte “funziona”, ma la diagnostica diventa scarsa e il comportamento strano. Vediamo i trabocchetti più comuni, così ci inciampi al massimo una volta (e solo a scopo didattico).
Errore n.1: catturare std::exception per valore (catch (std::exception e)).
Questo porta a object slicing: perdi il tipo dinamico dell'eccezione e con esso il messaggio utile di what() e talvolta altri dettagli. Risultato: in console appare qualcosa come “std::exception” invece di un testo utile. La buona abitudine: catch (const std::exception& e).
Errore n.2: usare solo catch (...) e pensare che basti.
catch (...) cattura “tutto”, ma non ti dà what(), perché non hai un oggetto tipizzato. Conclusione: catturi l'errore ma non sai quale sia. Il minimo ragionevole è avere catch (const std::exception& e) e stampare e.what(), lasciando catch (...) come ultima linea di difesa.
Errore n.3: conservare un const char* da what() e usarlo dopo l'uscita dal catch.
Dentro il catch spesso funziona, ma dopo può diventare “spazzatura” o causare comportamento indefinito. Se ti serve il messaggio più tardi, copialo: std::string msg = e.what();. È un'operazione semplice che ti salva molti nervi.
Errore n.4: fare un catch vuoto e “ingoiare” l'errore.
A volte il principiante scrive catch (...) {} o catch (const std::exception&) {} “per non far crashare” e il programma continua in uno stato corrotto. È come coprire la spia «Check Engine» con nastro isolante: l'indicatore è pulito, ma sotto c'è ancora il problema. Se catturi un'eccezione devi avere una strategia: stampare il messaggio, interrompere l'operazione, ritornare il controllo più in alto.
Errore n.5: mettere catch (const std::exception& e) prima degli handler più specifici.
Se l'handler generale sta prima, gli handler specifici non verranno mai raggiunti. Non è solo “scorretto” esteticamente, può anche rompere la logica della gestione degli errori. Mantieni l'ordine: tipi concreti prima, std::exception dopo, catch (...) per ultimo.
GO TO FULL VERSION