CodeGym /Corsi /C++ SELF /std::exception e what(): pratica di base

std::exception e what(): pratica di base

C++ SELF
Livello 53, Lezione 1
Disponibile

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:

  1. Prima — i tipi più concreti (se prevedi di gestirli in modo specifico).
  2. Poi — l'handler generale const std::exception&.
  3. 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.

1
Compito
C++ SELF, livello 53, lezione 1
Bloccato
Il pulsante di emergenza
Il pulsante di emergenza
1
Compito
C++ SELF, livello 53, lezione 1
Bloccato
La guardia al varco
La guardia al varco
1
Compito
C++ SELF, livello 53, lezione 1
Bloccato
Passaggio onesto
Passaggio onesto
1
Compito
C++ SELF, livello 53, lezione 1
Bloccato
Divisione con nota
Divisione con nota
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION