Nell’articolo sugli errori di programmazione (link articolo) abbiamo visto che gli errori di runtime sono quegli errori che non possono essere rilevati in fase di compilazione, perché si manifestano solo durante la fase di esecuzione del programma e solo in alcune particolari circostanze, ossia al verificarsi di “eventi eccezionali”. Alcuni esempi di errori di runtime sono: il verificarsi di una divisione per 0; l’utilizzo di un pathname non valido o che fa riferimento ad una periferica che in un certo momento risulta scollegata; il verificarsi di un overflow su un tipo di dato da parte di un’operazione aritmetica; un’operazione di casting non valido (ad esempio, quando si ha l’inserimento in input da parte dell’utente di una stringa di testo non interpretabile come un numero e che viene assegnata ad una variabile di tipo numerico).
Si tratta di errori pericolosi perché se non gestiti, possono generare anomalie di funzionamento che possono determinare persino il blocco inaspettato del programma (crash dell’applicazione). La gestione di questo tipo di errori non è banale ed è per questo che per facilitarla i linguaggi di programmazione ad oggetti (OOP) mettono a disposizione il meccanismo delle eccezioni.
Il meccanismo delle eccezioni
Quando si verifica un errore di runtime i linguaggi OOP, si dice, possono “sollevare” o permettono di “lanciare” (in inglese, to throw) un’eccezione. Ciò in generale di traduce nella creazione di un oggetto che appartiene ad una classe particolare, dipendente dallo specifico errore di runtime che si è verificato. In altri termini, il verificarsi di un errore di runtime costituisce l’evento in corrispondenza del quale, viene creato un oggetto detto eccezione. Tale oggetto può essere utilizzato dal programma per avere informazioni circa l’errore che l’ha creato e rimediare ad esso tramite le istruzioni inserite in un gestore strutturato delle eccezioni. Questo meccanismo, infatti, prevede che quando viene sollevata un’eccezione, il flusso di esecuzione del programma viene sospeso nel punto in cui si è verificato l’errore e salta in un altro punto del codice in cui esso viene gestito. Nel linguaggio C++ ciò viene realizzato utilizzando il costruttto try-catch:
1 2 3 4 5 6 7 8 9 10 11 12 |
try{ //istruzioni a rischio errori di runtime } catch(TipoErrore1){ //istruzioni che gestiscono le eccezioni di tipo 'TipoErrore1' } catch(TipoErrore2){ //istruzioni che gestiscono le eccezioni di tipo 'TipoErrore2' } catch(...){ //istruzioni che gestiscono tutte le altre eccezioni } |
- try è la parola chiave che introduce il blocco in cui si possono inserire le istruzioni che si vuole tenere “sotto controllo” perché durante l’esecuzione potrebbero generare degli errori di runtime.
- catch è la parola chiave che introduce un blocco in grado di riconoscere e “catturare” un certo tipo di eccezione e che contiene le istruzioni per gestire l’errore di runtime che l’ha generata.
- Un bloccco try può avere ad esso associati uno o più blocchi catch: quando all’interno di un blocco try viene sollevata o lanciata un’eccezione, il flusso di esecuzione del programma salta ai suoi blocchi catch, partendo dal primo e proseguendo verso gli altri finché non si trova un blocco catch (se esiste) che cattura il tipo di eccezione sollevata. Quando si entra in un blocco catch, alla sua uscita il flusso del programma prosegue oltre l’ultimo blocco catch.
Un’eccezione può essere o sollevata direttamente dall’istruzione in corrispondenza della quale si verifica l’errore, oppure se per una certa istruzione ciò non è previsto dal linguaggio, può essere lanciata esplicitamente con un’istruzione throw con la seguente sintassi:
throw espressione;
- throw è la parola chiave con cui è possibile lanciare un’eccezione per segnalare il fatto che si è verificato un errore. L’eccezione viene “marcata” opportunamente come viene spiegato di seguito, per permettere il suo riconoscimento da parte dei blocchi catch.
- Il valore calcolato dell’espressione è detto “valore dell’eccezione” (spesso è una stringa utilizzata per descrivere l’errore) e il suo tipo è di importanza fondamentale in quanto costituisce “la marcatura” con cui un blocco catch può individuare e catturare l’eccezione. Se nei linguaggi OOP puri come Java, un’eccezione è sempre un oggetto, in C++ invece un’eccezione può essere più in generale un dato di un tipo qualsiasi, passato all’istruzione throw che “lancia” l’eccezione.
Che cosa significa ciò lo vediamo con il seguente esempio.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <iostream> using namespace std; int main() { double x, y; try { cout<<"Inserisci il dividendo: "; cin>>x; cout<<"Inserisci il divisore: "; cin>>y; //monitora il verificarsi dell'errore di runtime di divisione per zero if(y==0) { //lancia un'eccezione di tipo 'costante stringa' throw "Divisione per zero!"; } cout<<x/y<<endl; } //individua e cattura un'eccezione di tipo 'costante stringa' catch (const char* messaggio) { cerr<<messaggio<< endl; } return 0; } |
Tra le istruzioni del blocco try dell’esempio di sopra, in particolare nella riga 16, è stata individuata un’istruzione che può generare un errore di runtime di ‘divisione per zero’, pertanto essa viene controllata con un istruzione throw che lancia un’eccezione al verificarsi di tale errore. L’eccezione lanciata è di tipo ‘costante stringa’, che è un tipo di eccezione individuato e catturato dal blocco catch della riga 19. In questo programma quando la variabile y assume il valore 0, si genera un errore di runtime e la riga 14 lancia l’eccezione che determina un salto nella riga 19 del blocco catch in cui tale errore viene gestito. In questo caso la gestione dell’errore consiste solo nella scrittura sullo standard error del messaggio ‘Divisione per zero!’.
Ricapitolando:
Il blocco try contiene il codice ‘a rischio eccezioni’ e può segnalare eventi di errore di runtime sollevando o lanciando eccezioni. E’ buona norma inserire in questo blocco il solo codice che potenzialmente può sollevare eccezioni. Se non si verificano eccezioni, l’esecuzione del blocco try è quella standard.
Il blocco catch contiene il codice per gestire gli errori di runtime. Esso ha un parametro (posto tra parentesi) che specifica quale tipo di valore di eccezione può essere intercettato. Questo valore può essere anche utilizzato all’interno del blocco. Se nel blocco try non viene sollevata alcuna eccezione, l’esecuzione del blocco try viene completata e il blocco catch viene ignorato. Al contrario, se nel blocco try viene sollevata un’eccezione, il blocco try non viene completato e si salta nel blocco catch. Se non c’è un blocco catch del tipo opportuno, il programma termina. Una volta completato il blocco catch, viene eseguito il codice che segue. Un blocco catch risponde solo ad un blocco try immediatamente precedente. Poiché un blocco try può potenzialmente sollevare più eccezioni di tipi diversi (in tempi diversi) e poiché un blocco catch può intercettare valori di un solo tipo, un blocco try può essere fatto seguire anche da una sequenza di più blocchi catch. In tal caso per l’individuazione dell’eccezione, i blocchi catch vengono scansionati nello stesso ordine in cui compaiono scritti, dal primo all’ultimo, entrando nel primo blocco catch che ha il parametro dello stesso tipo dell’eccezione sollevata. Una volta terminata l’esecuzione del blocco catch che ha catturato l’eccezione, l’esecuzione salta oltre l’ultimo blocco catch. Esiste un blocco catch speciale che intercetta ogni tipo di eccezione (il suo parametro è sostituito da tre punti), da usare come blocco catch di default:
catch(…) {cout<<“Eccezione non specificata!”;}
Questo blocco catch, intercettando tutte le eccezioni, ovviamente, va inserito sempre come ultimo blocco catch, altrimenti renderebbe inutili gli altri.
Propagazione delle eccezioni
Un’eccezione non necessariamente deve essere gestita all’interno della stessa funzione in cui viene sollevata o lanciata, ma al contrario è opportuno che questa gestione avvenga al suo esterno, all’interno di una funzione chiamante. Spesso, infatti, accade che la sua gestione dipende e può essere diversa a seconda del punto in cui quella funzione viene chiamata. Questo è reso possibile dal meccanismo proprio delle eccezioni, il quale prevede che un’eccezione sollevata in una funzione e che non venga in essa gestita, sia propagata all’indietro verso il chiamante, si dice risalendo lo stack delle chiamate, finché non incontra il primo blocco try-catch che prende in carico la sua gestione. Vediamo un semplice esempio:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#include <iostream> using namespace std; double dividi(int a, int b) { if(b==0) { //individua l'errore di runtime //lancia l'eccezione di divisione per zero utilizzando una stringa throw "Divisione per zero!"; } return (a/b); } int main() { int x=10; int y=0; double z=0; try { z = dividi(x, y); cout<<z<<endl; }catch (const char* messaggio) { //intercetta un'eccezione di tipo costante stringa cerr<<messaggio<< endl; } return 0; } |