Menu

Try-catch in C++: gestire le eccezioni nel modo giusto

Racchiudi il codice rischioso in try e reagisci in catch. Impara a catturare le eccezioni per riferimento const, ordinare più gestori, usare catch (...) e rilanciare, senza perdere risorse.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Dal lancio alla gestione

Nella pagina precedente hai imparato a lanciare un'eccezione con throw quando qualcosa va storto. Lanciare è solo metà della storia: un'eccezione che non viene mai catturata chiama std::terminate e fa crashare il programma. L'istruzione try/catch è il modo per gestire ciò che è stato lanciato e continuare l'esecuzione.

La forma è semplice: metti il codice rischioso dentro un blocco try e fallo seguire da uno o più blocchi catch che reagiscono a tipi di errore specifici. Se il blocco try va a buon fine, ogni catch viene saltato. Nel momento in cui qualcosa lancia un'eccezione, il controllo salta subito al primo catch corrispondente.

Nota che "after" non viene mai stampato. Appena scatta il throw, il resto del blocco try viene abbandonato e l'esecuzione riprende dentro il catch corrispondente. Quando il catch termina, il programma continua normalmente dopo di esso.

Catturare per riferimento const

L'abitudine più importante nella gestione degli errori in C++: cattura le eccezioni per riferimento const, non per valore.

Catturare per valore copia l'eccezione e, peggio ancora, la taglia (slicing). Le eccezioni standard formano una gerarchia (runtime_error e logic_error derivano entrambe da std::exception), quindi catturare un'eccezione derivata come valore di tipo base ne taglia via la parte derivata. Catturare per riferimento mantiene l'oggetto intatto e polimorfico:

Qui lanciamo un out_of_range ma lo catturiamo come const exception&. Dato che out_of_range deriva da exception, il gestore della classe base corrisponde, e il riferimento fa sì che e.what() restituisca ancora il messaggio reale. Se avessi scritto catch (exception e) (per valore), l'oggetto sarebbe stato ridotto a una semplice exception e potresti perdere il messaggio specifico.

Più blocchi catch

Un singolo try può essere seguito da diversi blocchi catch, ognuno per un tipo di eccezione diverso. Il C++ li prova dall'alto verso il basso ed esegue il primo che corrisponde, quindi ordinali dal più specifico al più generico.

Dato che invalid_argument è più specifico di exception, deve venire prima. Se invertissi l'ordine mettendo catch (const exception&) in cima, inghiottirebbe ogni eccezione: il gestore invalid_argument sotto di esso diventerebbe codice morto che non può mai essere eseguito. Molti compilatori avvisano di questo, ma il linguaggio non ti ferma.

catch (...) e rilancio

A volte vuoi una rete di sicurezza per tutto ciò che non avevi previsto. Il gestore universale catch (...) corrisponde a ogni tipo di eccezione, comprese quelle che non derivano da std::exception (qualcuno può scrivere throw 42; o throw "oops";).

Il problema è che non ricevi nessun oggetto: non c'è una e da esaminare. Per questo catch (...) va usato soprattutto come ultima risorsa: registra che qualcosa è fallito, oppure fai pulizia e rilancia.

Per rilanciare l'eccezione corrente, cioè passarla a un gestore più esterno dopo aver fatto un po' di pulizia locale o di logging, usa un semplice throw; senza operando. In questo modo conservi l'eccezione originale (il suo tipo reale e il messaggio), a differenza di throw e;, che rilancerebbe una copia tagliata:

Il gestore interno registra e rilancia; poi se ne occupa il gestore esterno in main. Usa throw; da solo per questo, mai throw e;.

Stack unwinding e RAII

Quando un'eccezione si propaga fuori da un blocco try, il C++ esegue lo stack unwinding (lo srotolamento dello stack): ogni oggetto locale tra il throw e il catch corrispondente vede chiamato il proprio distruttore, in ordine inverso rispetto alla costruzione. È questo che rende sicure le eccezioni: le risorse tenute dagli oggetti sullo stack vengono rilasciate in automatico.

Ecco esattamente perché dovresti tenere le risorse in tipi RAII (come std::vector, std::string e gli smart pointer) invece di usare new/delete a mano. Guarda cosa succede quando un'eccezione attraversa un'allocazione manuale:

void leaky() {
    int* buffer = new int[1000];
    mightThrow();        // se questa lancia, la riga successiva non viene mai eseguita...
    delete[] buffer;     // ...e il buffer resta allocato (memory leak)
}

Dato che il throw salta oltre delete[], la memoria va persa. Uno smart pointer risolve il problema gratis: il suo distruttore viene eseguito durante lo srotolamento:

void safe() {
    auto buffer = std::make_unique<int[]>(1000);
    mightThrow();   // se questa lancia, il distruttore di buffer libera comunque la memoria
}                   // nessun delete manuale, nessun leak, anche nel percorso dell'eccezione

In sintesi: non catturare un'eccezione con catch solo per fare delete di qualcosa. Lascia la pulizia ai distruttori e riserva catch alle decisioni su come riprenderti.

Errori comuni e insidie

Alcune trappole si ripresentano di continuo:

Non usare le eccezioni per il normale flusso di controllo. Lanciare e srotolare lo stack è molto più lento di un semplice if. Riserva le eccezioni a condizioni di errore davvero eccezionali, non a "l'utente ha scritto una stringa vuota".

Un blocco catch vuoto nasconde i bug. Scrivere catch (...) {} per zittire un errore significa che i fallimenti spariscono senza lasciare traccia. Come minimo registra il problema; di solito dovresti rilanciarlo o gestirlo come si deve.

Un distruttore che lancia è pericoloso. Se un distruttore lancia un'eccezione durante lo stack unwinding (mentre un'altra eccezione è già in volo), il programma chiama std::terminate. Nel C++ moderno i distruttori sono implicitamente noexcept: non lasciare mai che un'eccezione ne esca.

catch vede solo ciò che try copre. Un'eccezione lanciata prima di entrare nel try, o in un'altra funzione che non si trova sul percorso di chiamata al suo interno, non verrà catturata qui. Il catch protegge solo il codice eseguito dentro il suo try (direttamente o nelle funzioni che chiama).

Prossimo: undefined behavior

Le eccezioni sono il modo definito con cui il C++ ti dice che qualcosa è andato storto: lanci, catturi, e il comportamento è prevedibile. Ma il C++ ha anche un angolo più oscuro in cui il linguaggio non promette proprio nulla: dereferenziare un puntatore pendente, leggere oltre la fine di un array, l'overflow degli interi con segno. La prossima pagina parla dell'undefined behavior (comportamento indefinito): cosa lo provoca, perché può sembrare "funzionare" fino al momento in cui crolla in modo catastrofico, e come tenerlo fuori dal tuo codice.

Domande frequenti

Come funziona try-catch in C++?

Metti il codice che potrebbe lanciare un'eccezione dentro un blocco try { }. Se viene lanciata un'eccezione, il programma smette di eseguire il resto del blocco try e salta al primo blocco catch corrispondente, dove gestisci l'errore. Se non viene lanciato nulla, i blocchi catch vengono saltati del tutto.

Perché catturare le eccezioni per riferimento const in C++?

Catturare per riferimento (catch (const std::exception& e)) evita di copiare l'oggetto eccezione e, soprattutto, preserva il polimorfismo: un'eccezione derivata catturata come tipo base chiama comunque il what() giusto. Catturare per valore (catch (std::exception e)) taglia via la parte derivata e può perdere informazioni.

Come si cattura qualsiasi eccezione in C++?

Usa catch (...): i puntini di sospensione catturano ogni eccezione, di qualunque tipo. È utile come gestore di ultima istanza, ma dato che non ricevi nessun oggetto da esaminare, mettilo dopo i blocchi catch specifici e usalo soprattutto per registrare l'errore o rilanciarlo.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA