Eccezioni

 


 

Segnalazione e gestione degli errori

 

Il termine eccezione (dall'inglese exception) deriva dall'ottimistica assunzione che nell'esecuzione di un programma gli errori costituiscano una "circostanza eccezionale". Anche condividendo tale ottimismo, il problema di come individuare gli errori e di come gestirli una volta individuati deve essere sempre affrontato con grande cura nella progettazione di un programma.

Anche in un programma "perfetto" gli errori in fase di esecuzione possono sempre capitare, perchè sono commessi in larga parte da operatori "umani" (quelli che usano il programma), e quindi è lo stesso programma che deve essere in grado di prevederli e di eseguire le azioni di ripristino, quando è possibile.

Quando un programma, specie se di grosse dimensioni, è composto da moduli separati, e soprattutto se i moduli provengono da librerie sviluppate da altri programmatori, anche la gestione degli errori deve essere tale da minimizzare le dipendenze fra un modulo e l'altro. In generale, quando un modulo verifica una condizione di errore, deve limitarsi a segnalare tale condizione, in quanto l'azione di ripristino dipende più spesso dal modulo che ha invocato l'operazione piuttosto che da quello che ha riscontrato l'errore mentre cercava di eseguirla. Separando i due momenti (la rilevazione dell'errore e la sua gestione) si mantiene il massimo di indipendenza fra i moduli: l'interfaccia comune conterrà gli strumenti necessari, attivati dal modulo "rilevatore" e utilizzati dal modulo "gestore"

Il C++ mette a disposizione un meccanismo semplice ma molto efficace di rilevazione e gestione degli errori: l'idea base è che, quando una funzione rileva un errore che non è in grado di affrontare direttamente, l'esecuzione della funzione termina, ma il controllo non ritorna al punto in cui la funzione è stata chiamata,  bensì in un altro punto del programma, dove viene eseguita la procedura di gestione dell'errore. In termini tecnici, la funzione che rileva l'errore "solleva" o "lancia" (throw) un'eccezione ("marcandola" in qualche modo, come vedremo) e termina: l'area stack è ripercorsa all'indietro e cancellata (stack unwinding) a vari livelli finchè il flusso del programma non raggiunge il punto (se esiste) in cui l'eccezione può essere riconosciuta e "catturata" (catch); in questo punto viene eseguita la procedura di gestione dell'errore; se il punto non esiste l'intero programma "abortisce".

 


 

Il costrutto try

 

La parola-chiave try introduce un blocco di istruzioni.

Es. :             try    
{
m = c / b;
double f = 10.7;
res = fun(f ,m+n);
}

Le istruzioni contenute in un blocco try sono "sotto controllo": in esecuzione, qualcuna di esse potrebbe generare un errore. Nell'esempio, la funzione fun potrebbe chiamare un'altra funzione e questa un'altra ancora ecc... , generando una serie di pacchetti che si accumula sullo stack. L'area dello stack che va da un un blocco try in su è detta: exception stack frame e costituisce l'insieme di tutte le istruzioni controllate.

 


 

L'istruzione throw

 

Dal punto di visto sintattico, l'istruzione throw è identica all'istruzione return di una funzione (e si comporta all'incirca nello stesso modo):

throw espressione;

Un'istruzione throw può essere collocata soltanto in un exception stack frame e segnala il punto in cui si è ricontrato un errore (o, come si dice, è "sollevata" un'eccezione). Il valore calcolato dell'espressione, detto: "valore dell'eccezione" (il cui tipo è detto: "tipo dell'eccezione"), ripercorre "all'indietro" l'exception stack frame (cancellandolo): se a un certo punto del suo "cammino" l'eccezione viene "catturata" (vedremo come), l'errore può essere gestito, altrimenti il programma abortisce (ed è quello che succede in particolare se l'istruzione throw non è inserita all'interno di un exception stack frame).

In pratica throw si comporta come un return "multilivello". Il valore dell'eccezione viene di solito utilizzato per la descrizione dell'errore commesso (non è però obbligatorio utilizzarlo). Il suo tipo è invece di importanza fondamentale in quanto (come vedremo) costituisce la "marca" di riconoscimento dell'eccezione e ne permette la "cattura".

 


 

Il gestore delle eccezioni: costrutto catch

 

La parola-chiave catch introduce un blocco di istruzioni che ha lo stesso formato sintattico della definizione di una funzione, con un solo argomento e senza valore di ritorno.

catch (tipo argomento )    { .......... blocco di istruzioni .............. }

Fisicamente un blocco catch deve seguire immediatamente un blocco try. Dal punto di vista della successione logica delle operazioni, invece, un blocco catch costituisce il punto terminale di ritorno di un exception stack frame: questo viene costruito (verso l'alto), a partire da un blocco try, fino a un'istruzione throw, da cui l'eccezione "sollevata" ridiscende (stack unwinding) fino al blocco catch corrispondente al blocco try di partenza (oppure passa direttamente dal blocco try al blocco catch se l'istruzione throw si trova già nel blocco try di partenza; in questo caso l'istruzione throw non si comporta come un return, ma piuttosto come un goto). A questo punto l'eccezione può essere "catturata" o meno: se è catturata, vengono eseguite le istruzioni del blocco catch (detto "gestore dell'eccezione") e poi il flusso del programma prosegue normalmente; se invece l'eccezione non è catturata, il programma abortisce. Se infine non vengono sollevate eccezioni, cioè l'exception stack frame non incontra istruzioni throw, il flusso del programma ridiscende per vie normali tornando al blocco try da cui era partito, eseguito il quale prosegue "saltando" il successivo blocco catch.

Un'eccezione viene "catturata" se il suo tipo coincide esattamente con il tipo dell'argomento di catch. Non sono ammesse conversioni di tipo, neppure implicite (neanche se i due tipi sono uguali in pratica, come int e long in una machina a 32 bit). Verificata la coincidenza dei tipi, il valore dell'eccezione viene trasferito nell'argomento di catch (come se l'istruzione throw "chiamasse" la "funzione" catch); il trasferimento avviene normalmente per copia (by value), a meno che l'argomento di catch non sia un riferimento, nel qual caso il passaggio è by reference, che però ha senso solo se l'espressione di throw è un l-value e se "sopravvive" alla distruzione dello stack (cioè è un oggetto globale, o è definito in un namespace, oppure è locale ma dichiarato static). E' possibile anche che l'argomento di catch sia dichiarato const, nel qual caso valgono le stesse regole e limitazioni che ci sono per il passaggio degli argomenti delle funzioni (vedere il capitolo: Puntatori e costanti - Passaggio degli argomenti trasmessi by value e by reference).

Nel costrutto catch la specifica dell'argomento non è obbligatoria (lo è solo se l'argomento viene usato nel blocco di istruzioni). Il tipo, invece, deve essere sempre specificato, perchè serve per la "cattura" dell'eccezione. A questo proposito è utile aggiungere che la scelta del tipo dell'eccezione è libera, ma, per una migliore leggibilità del programma e per evitare confusioni con le altre eccezioni (in special modo con quelle generate dalle librerie del sistema, fuori dal nostro controllo), è vivamente consigliata la creazione di tipi "ad hoc", preferibilmente uno per ogni possibile eccezione e con attinenza mnemonica fra il nome del tipo e il significato dell'errore a cui è associato: quindi, evitare l'uso di tipi nativi (anche se non sarebbe vietato), ma usare solo tipi astratti (per esempio strutture con nomi "ad hoc").

E' bene che il  trattamento delle eccezioni venga usato quando la rilevazione e la gestione di un errore devono avvenire in parti diverse del programma. Quando invece un errore può essere trattato localmente è sufficiente servirsi dei normali controlli del linguaggio (come i costrutti if o switch).

NOTA: per completezza precisiamo che un'eccezione può essere "catturata" anche quando il suo tipo è di una classe "derivata" da quella a cui appartiene l'argomento di catch, ma di questo parleremo quando tratteremo delle classi e dell'eredità.

[p40][p40] [p40]

 


 

Riconoscimento di un'eccezione fra diverse alternative

 

Finora abbiamo detto che a un blocco try deve sempre seguire blocco catch. In realtà i blocchi catch possono anche essere più di uno, disposti consecutivamente e con tipi di argomento diversi.

Quando un'eccezione, discendendo lungo l'exception stack frame, incontra una serie di blocchi catch, il suo tipo viene confrontato a uno a uno con quelli dei blocchi catch e, se si verifica una coincidenza, l'eccezione viene "catturata" e vengono eseguite le istruzioni del blocco catch in cui la coincidenza è stata trovata. Dopodichè il flusso del programma "salta" gli eventuali blocchi catch successivi e riprende normalmente dalla prima istruzione dopo l'ultimo blocco catch del gruppo. Il programma abortisce se nessun blocco catch cattura l'eccezione. Se invece non vengono sollevate eccezioni, il flusso del programma, eseguite le istruzioni del blocco try, "salta" tutti i blocchi catch del gruppo.

Se un costrutto catch, al posto del  tipo e dell'argomento, presenta "tre puntini" (ellipsis), significa che è in grado di catturare qualsiasi eccezione, indipendentemente dal suo tipo.

L'ordine in cui appaiono i diversi blocchi catch associati a un blocco try è importante: infatti il confronto con il tipo dell'eccezione da catturare viene sempre fatto a partire dal primo blocco catch che segue il blocco try e procede nello stesso ordine: da ciò consegue che l'eventuale catch con ellipsis deve essere sempre l'ultimo blocco del gruppo. L'esempio che segue schematizza la situazione di un blocco try seguito da tre blocchi catch, di cui l'ultimo con ellipsis.

try  { blocco try } se non solleva eccezioni, esegue blocco try e salta a istruzione                     
catch (tipo1) { blocco1} altrimenti, se il tipo dell'eccezione coincide con tipo1, cattura l'eccezione, esegue blocco1 e salta a  istruzione
catch (tipo2) { blocco2} altrimenti, se il tipo dell'eccezione coincide con tipo2, cattura l'eccezione, esegue blocco2 e salta a  istruzione
catch (...)    {blocco3} altrimenti, cattura comunque l'eccezione ed esegue blocco3
 
istruzione ......... riprende il flusso normale del programma

 


 

Blocchi innestati

 

Una sequenza di blocchi try....catch può essere a sua volta "innestata" in un blocco try o in un blocco catch (o in una funzione chiamata, direttamente o indirettamente, da un blocco try o da un blocco catch).

Se la nuova sequenza è interna a un blocco try (cioè nella fase "ascendente" dell'exception stack frame) e successivamente viene sollevata un'eccezione, il controllo per la cattura dell'eccezione viene fatto anzitutto sui blocchi catch interni (che sono incontrati prima nella fase di stack unwinding): se l'eccezione è catturata, il problema è risolto e anche tutti i blocchi catch associati al blocco try esterno vengono "saltati"; se invece nessun blocco interno cattura l'eccezione, il programma non abortisce, ma il controllo passa ai blocchi catch associati al blocco try esterno.

Se la nuova sequenza è interna a un blocco catch (cioè se l'eccezione è già stata catturata), si crea un nuovo exception stack frame a partire da quel punto: pertanto, se è sollevata una nuova eccezione e questa viene catturata, il programma esegue il blocco catch interno che ha catturato la nuova eccezione e poi completa l'esecuzione del blocco catch esterno che ha catturato l'eccezione precedente; se invece la nuova eccezione non è catturata, il programma abortisce.

Anche l'istruzione throw può comparire in un blocco catch o in una funzione chiamata, direttamente o indirettamente, da un blocco catch (la sua collocazione "normale" sarebbe invece in un blocco try o in una funzione chiamata, direttamente o indirettamente, da un blocco try). In questo caso si dice che l'eccezione è "ri-sollevata", ma non può essere gestita allo stesso livello del blocco catch da cui parte, in quanto un blocco catch non può essere "chiamato" ricursivamente. Pertanto un'eccezione sollevata dall'interno di un blocco catch non fa abortire il programma solo se lo stesso blocco catch fa parte di una sequenza innestata in un blocco try esterno (e saranno i corrispondenti blocchi catch a occuparsi della sua cattura).
Un caso particolare di eccezione "ri-sollevata" si ha quando l'istruzione throw appare da sola, senza essere seguita da un'espressione; in questo caso il valore e il tipo dell'eccezione sono gli stessi del blocco catch in cui l'istruzione throw è inserita (cioè il programma "ri-solleva" la stessa eccezione che sta gestendo).

[p41]

 


 

Eccezioni che non sono errori

 

Come abbiamo detto all'inizio, il concetto di  eccezione è di norma legato a quello di errore. Tuttavia il meccanismo di gestione delle eccezioni altro non è che un particolare algoritmo di "controllo", meno strutturato e meno efficiente rispetto alle strutture di controllo locali (quali if, switch, for ecc...), che però permette operazioni, come i return "multilivello", che con le strutture tradizionali sarebbero più difficili da ottenere o porterebbero a un codice non in grado di mantenere un adeguato livello di indipendenza fra i diversi moduli del programma.

Quindi la convenienza o meno dell'utilizzo delle eccezioni non si basa tanto sulla distinzione fra errori o altre situazioni, quanto piuttosto sul fatto che le  due operazioni di "controllo" e "azione conseguente" siano localizzate insieme (nel qual caso conviene usare le strutture tradizionali), oppure siano separate in aree diverse dello stack (e allora è preferibile usare le eccezioni).

Per esempio, l'utilizzo delle eccezioni come strutture di controllo potrebbe essere una tecnica elegante per terminare funzioni di ricerca, soprattutto se la ricerca avviene attraverso chiamate ricorsive, che "impilano" un numero imprecisato di pacchetti sullo stack.

Altre "correnti di pensiero", invece, suggersicono di mantenere strettamente correlato il concetto di eccezione con quello di errore, per evitare la generazione di codice ambiguo e poco comprensibile (e quindi meno portabile e, in sostanza, "più costoso").

[p42][p42] [p42]

 


 

Torna all'Indice