Skip to content

Eccezioni e Gestione degli Errori

Le eccezioni sono tipi di dato progettati per comunicare una condizione di errore. Quando si verifica un problema imprevisto o irrecuperabile a livello locale (come all’interno del Costruttore di un oggetto, che non può “ritornare” un codice d’errore), si lancia (throw) un’eccezione.

Per lanciare un’eccezione si usa throw seguito da un oggetto lanciabile. La libreria standard del C++ (<stdexcept>) definisce diverse classi preimpostate per questo scopo, come std::runtime_error.

#include <stdexcept>
void validate_age(int age) {
if (age < 0) {
throw std::runtime_error{ "L'età non può essere negativa!" };
}
}

Dopo essere stata lanciata, l’eccezione interrompe il flusso normale del programma e inizia una ricerca all’indietro nello stack (pila delle chiamate di funzione) per trovare un “cacciatore” adeguato: il blocco catch. Il codice potenzialmente problematico va inserito nel blocco try.

Puoi avere molteplici blocchi catch di fila per gestire tipi differenti di eccezioni. Esiste anche un blocco “catch-all” estremo che usa i puntini di sospensione: catch (...) { }, il quale cattura qualsiasi cosa venga lanciata, ma senza poterne analizzare il contenuto.

Eccezioni Standard: Logic Errors vs Runtime Errors

Section titled “Eccezioni Standard: Logic Errors vs Runtime Errors”

Esiste una gerarchia (basata sull’ereditarietà) in <stdexcept> che discende dalla classe padre std::exception:

  1. Logic Errors: Errori logici derivanti da std::logic_error (domain_error, invalid_argument, out_of_range). Sono errori causati da una disattenzione del programmatore, come parametri sbagliati o accessi ad array fuori dai limiti. Potevano essere evitati con codice più stringente.
  2. Runtime Errors: Errori derivanti da std::runtime_error (system_error, overflow_error). Derivano dall’ambiente, dal sistema operativo o da eventi incontrollabili durante l’esecuzione del codice (disco pieno, permessi negati, memoria esaurita).

Quando un’eccezione “è in volo” (in flight), il programma scarta i blocchi in cui si trovava. Tutte le variabili automatiche che escono di scope durante questo processo vengono distrutte: viene chiamato regolarmente il loro distruttore. Questo processo è noto come Stack Unwinding (Svolgimento della Pila).

Questo meccanismo è il fulcro di un pattern cruciale del C++ noto come RAII (Resource Acquisition Is Initialization), o Constructor Acquires, Destructor Releases. L’idea è che tu deleghi la deallocazione di un buffer o la chiusura di un file all’interno del distruttore dell’oggetto che lo controlla. Così, in caso di eccezioni fatali, lo stack unwinding garantirà l’invocazione dei distruttori e tu non lascerai mai risorse aperte/memory leaks alle tue spalle.

Mai lanciare nei distruttori: Se un distruttore invocato durante lo stack unwinding lanciasse a sua volta un’altra eccezione, il C++ si troverebbe con due eccezioni attive nello stesso tempo. In questo caso, il runtime C++ invocherà la chiusura catastrofica del programma (std::terminate).

Se scrivi una funzione (o un distruttore) che sai per certo non potrà mai lanciare eccezioni, dovresti marcare la sua firma con noexcept:

bool is_odd(int x) noexcept {
return (x % 2) != 0;
}

Questo funge da promessa blindata per il compilatore. Se un’eccezione cercasse di scappare da una funzione noexcept, il programma verrebbe interrotto brutalmente. Il grande vantaggio è che il compilatore disattiva le protezioni e ottimizza fortemente il codice interno di queste funzioni, permettendoti performance estreme.