Gestione delle Eccezioni in C++: Try, Catch e Throw

LT
Luca Terribili
Autore

Un'eccezione rappresenta un evento anomalo – ad esempio una divisione per zero, l'impossibilità di aprire un file o un errore di memoria – che interrompe il normale flusso di esecuzione. Invece di terminare il programma, l'eccezione può essere lanciata (throw) e poi gestita in un blocco dedicato (catch), consentendo al programma di riprendere o di chiudersi in modo controllato.

L'adozione di una gestione delle eccezioni ben progettata rende il codice più leggibile, facilita il debug e permette di isolare le parti critiche dal resto dell’applicazione. Nei paragrafi seguenti approfondiremo il concetto di eccezione, illustreremo la sintassi delle strutture di gestione e presenteremo esempi pratici e tecniche avanzate con classi personalizzate.

Che cos'è un'eccezione e quando usarla

Le eccezioni sono meccanismi che interrompono temporaneamente il flusso normale del programma quando si verifica un evento imprevisto. Sono particolarmente utili in situazioni che non possono essere anticipate con semplici controlli, come errori di I/O, problemi di allocazione della memoria o condizioni aritmetiche illegali.

È consigliabile usare le eccezioni quando un errore è così significativo da rendere impossibile proseguire con la normale logica del metodo corrente. Per esempio, se un’applicazione deve leggere un file e il file non è presente o non è accessibile, lanciare un’eccezione consente di segnalare l’impossibilità di continuare e di dare al chiamante la possibilità di gestire il problema (ad esempio chiedendo un nuovo percorso).

L'uso parsimonioso delle eccezioni è fondamentale: dovrebbero essere impiegate solo per condizioni eccezionali e non per controlli di flusso di routine, altrimenti il codice rischia di diventare più difficile da comprendere e mantenere.

La sintassi di try, catch e throw in C++

  • try delimita il blocco di codice potenzialmente soggetto a errori.
  • catch cattura le eccezioni lanciate all’interno del try; è possibile specificare più blocchi catch per gestire tipi diversi di eccezioni.
  • throw genera un’eccezione, trasportandola verso il più vicino blocco catch compatibile.
#include <iostream>

void divide(int a, int b) {
    if (b == 0)
        throw std::runtime_error("Divisione per zero");
    std::cout << "Risultato: " << a / b << std::endl;
}

int main() {
    try {
        divide(10, 0);
    } catch (const std::exception& e) {
        std::cerr << "Errore: " << e.what() << std::endl;
    }
    return 0;
}

Nel codice sopra, divide lancia un’eccezione quando il divisore è zero; il catch nel main intercetta l’oggetto std::runtime_error e stampa un messaggio descrittivo. Ricordiamo che le eccezioni in C++ sono oggetti, quindi è possibile definire classi di eccezione personalizzate per fornire informazioni più specifiche.

Esempi pratici di gestione delle eccezioni

I/O

#include <fstream>
#include <stdexcept>

int main() {
    std::ifstream file("file.txt");
    try {
        if (!file.is_open())
            throw std::runtime_error("Impossibile aprire file.txt");
        // Elaborazione del file
    } catch (const std::exception& e) {
        std::cerr << "Eccezione I/O: " << e.what() << std::endl;
    }
    return 0;
}

Il try tenta di aprire il file; in caso di fallimento, viene lanciata un'eccezione di tipo std::runtime_error catturata dal catch, che stampa il messaggio di errore.

Aritmetica

#include <stdexcept>

int main() {
    int a = 10, b = 0;
    try {
        if (b == 0)
            throw std::invalid_argument("Divisore nullo");
        int result = a / b;
        // Uso di result
    } catch (const std::exception& e) {
        std::cerr << "Eccezione aritmetica: " << e.what() << std::endl;
    }
    return 0;
}

Qui, un controllo preventivo su b evita la divisione per zero lanciando un’eccezione di tipo std::invalid_argument, gestita in modo analogo al caso precedente.

Questi esempi mostrano come le strutture try/catch/throw permettano di isolare errori comuni e di mantenere il programma stabile anche in presenza di condizioni avverse.

Migliorare la gestione delle eccezioni con le classi personalizzate

Definire classi di eccezione specifiche consente di trasmettere informazioni più ricche al chiamante. È sufficiente ereditare da std::exception (o da una sua sottoclasse) e ridefinire il metodo what().

class MyException : public std::exception {
public:
    const char* what() const noexcept override {
        return "Si è verificata un'eccezione personalizzata";
    }
};

L'eccezione può essere lanciata con throw MyException(); e catturata tramite un catch dedicato:

try {
    // Codice rischioso
    throw MyException();
} catch (const MyException& e) {
    std::cerr << "Gestione specifica: " << e.what() << std::endl;
}

Utilizzare eccezioni personalizzate permette di distinguere chiaramente tra errori di natura diversa e di fornire al gestore contesti più dettagliati.

Considerazioni finali sulla gestione delle eccezioni in C++

Una gestione efficace delle eccezioni richiede una pianificazione attenta: identificare i punti critici, lanciare eccezioni appropriate e scrivere blocchi catch specifici. La gerarchia delle eccezioni, basata su std::exception, facilita la creazione di classi personalizzate e l’organizzazione di handler generali o specifici.

È importante ricordare che le eccezioni non dovrebbero sostituire il normale controllo di flusso; il loro ruolo è limitato a situazioni anomale e impreviste. Un uso eccessivo o improprio può complicare la lettura del codice e degradare le prestazioni.

Adottare le parole chiave try, catch e throw insieme a classi di eccezione ben progettate rende i programmi C++ più robusti, mantenibili e pronti a fronteggiare gli errori in modo controllato.