User Tools

Site Tools


appunti3s:eccezioni

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revision Previous revision
appunti3s:eccezioni [2019/07/27 10:51]
profpro corretto codice
appunti3s:eccezioni [2020/06/08 22:19] (current)
Line 1: Line 1:
 +> indice > [[appunti3s:linguaggio_c]]
  
 +====== Gestione delle eccezioni ====== 
 +
 +===== Premessa =====
 +Quando il programmatore deve scrivere un programma spesso incontra errori di diverso tipo:
 +  - errori di compilazione
 +  - errori di linking 
 +  - errori di esecuzione (run-time)
 +  - errori di correttezza dei risultati
 +I primi due tipi di errori impediscono la generazione del programma, ma anche un programma che sembra scritto senza errori può avere un comportamento indesiderato durante l'esecuzione (o nei risultati che produce).
 +
 +Il terzo tipo di errori può rientrare nella categoria dei cosiddetti "bug": un saltuario comportamento anomalo in esecuzione che impedisce l'ottenimento dei risultati. Il risultato può essere la chiusura imprevista del programma e l'unico modo di superarli è sempre la modifica del codice del programma. (Provare la divisione di due numeri...)
 +
 +Il quarto tipo è detto anche errori di logica. È meno grave del precedente perché il programma riesce almeno a produrre sempre dei risultati, solo che i risultati non sono sempre corretti. A volte sono causati dalle richieste dell'utente, ma in tal caso quest'ultimo tipo può essere previsto dal programmatore e opportunamente gestito. L'oggetto di queste pagine è l'analisi del metodo usato nella gestioni di questo tipo di errori.
 +===== Confronto tra C e C++ =====
 +
 +In C si controllava il buon esito di una funzione controllando il valore restituito (0 = nessun errore). In caso di segnalazione di un errore (restituzione di valore non zero) in C si chiamava la funzione //exit()// oppure //abort()// lasciando gli eventuali oggetti dinamici allocati in memoria.
 +Per evitare questo inconveniente ed usare un sistema ben organizzato, in C++ è sempre meglio usare le //eccezioni//
 +
 +In ogni caso, indipendentemente dalla strategia usata, il programmatore che vuole minimizzare gli errori, cerca di prevenirli usando strumenti con il //debugger// e il //testing//.
 +===== Quando si usano le eccezioni =====
 +
 +Le eccezioni sono oggetti (di tipo classe) che, quando un problema, vengono creati e passati come il parametro di una funzione. A volte capita che l'utente, invece di inserire in input un numero intero, inserisca un carattere. Per verificare questa eventualità il programmatore potrebbe usare un'istruzione //if// che in caso affermativo generi solo un oggetto di tipo eccezione, senza preoccuparsi di quale azione eseguire per risolvere il problema. 
 +Gli oggetti di tipo eccezione possono contenere informazioni, come quella sul tipo di problema incontrato. Il programmatore dovrebbe gestire i problemi con le eccezioni quando quel tipo di problema non è frequente e il programmatore non vuole occuparsene in quel momento in quel pezzo di programma. Se ne occuperà un altro pezzo di programma (scritto forse da un altro programmatore).
 +
 +Ci sono due tipi di programmatori: coloro che scrivono il codice di nuove funzioni e coloro che usano le funzioni create da qualcun altro. Ovviamente ci possono essere programmatori che fanno entrambe le attività, ma comunque si tratta sempre di due diversi tipi di attività che si svolgono in momenti diversi.
 + 
 +Grazie alle eccezioni queste due attività si distinguono ancora meglio e scrivere il codice diventa ancora più facile:
 +  * il programmatore che scrive il codice delle funzioni indica agli altri (nelle dichiarazioni delle funz.) solo quale tipo di eccezioni potrebbero essere lanciate (e quando).
 +  * il programmatore che usa una funzione scritta da qualcun altro indica (nelle chiamate delle funz.) cosa deve fare il suo programma quando dovesse ricevere quel tipo di eccezione.
 +Ad esempio, nel caso in cui un programma non riesca a leggere il file specificato dall'utente, che cosa si deve fare? Creare un nuovo file? Avvisare l'utente? Ovviamente dipende dal programma...
 +Lanciare eccezioni è un sistema comodo per chi scrive, ma anche leggermente poco efficiente perché richiede anche di dover ripercorrere (srotolare) lo stack delle chiamate delle funzioni: un'operazione non velocissima...
 +
 +===== Definizioni =====    
 +
 +
 +  - Le funzioni che possono generare eccezioni devono contenere le condizioni (//if//) per individuare la presenza di un problema.
 +  - L'eccezione non è altro che un parametro che la funzione "lancia" all'esterno usando //throw();//.
 +    - <file c letturaPeso.h> void letturaPeso(int& n) throw(int);</file>
 +    - <file c letturaPeso.cpp> #include <iostream>
 +void letturaPeso(int& n) throw(int)
 +
 +  std::cout << "inserisci il tuo peso: " << std::endl;
 +  std::cin >> n;
 +  if ( n < 0 )
 +     throw (-1); // viene lanciato un intero 
 +}</file>
 +  - Chi usa queste funzioni deve racchiuderle dentro un blocco //try//  
 +  - Il blocco //try// è seguito da uno o più  blocchi //catch()//, che  racchiudono il codice di "gestione" dell'errore. 
 +    - <file c main.cpp> 
 +#include <iostream>
 +#include "letturaPeso.h" 
 +int main()
 +{  int peso;
 +   try {
 +        letturaPeso(peso);
 +       }
 +   catch(int k)   // blocco idoneo a catturare un intero
 +                // eseguito solo nel caso del lancio di un'eccezione
 +         if ( k=-1 )
 +            std::cerr << "Hai inserito un peso negativo..." << std::endl;
 +            return 0;  // si decide di terminare il main
 +        }
 +   std::cout << "Tu pesi esattamente " << peso << " kg " << std::endl; 
 +   return 0;
 +}</file>
 +  - Quando la funzione (usata dentro //try//) trova il problema, "lancia" l'eccezione all'esterno di questo blocco (//try//) . 
 +  - //throw();// ha la funzione di interrompere il flusso del programma dirigendolo verso un blocco //catch()//.
 +  - Dentro le parentesi tonde di ogni blocco //catch()// sarà indicato (come un parametro intero k) il tipo di eccezione che quel blocco sa "ricevere"
 +  - Il blocco catch dà la possibilità di chiudere il programma o di provare a ripetere l'operazione andata male o, ancora, di generare un opportuno messaggio di errore all'utente.
 +  - Dopo il blocco //try// ci possono essere uno o **più** blocchi //catch()// ognuno dei quali è specializzato nel ricevere un particolare tipo di parametro. Il controllo dell'esecuzione viene ripreso proprio in questo modo. L'ordine con cui vengono specificati i blocchi //catch()// è lo stesso in cui verranno eseguiti. 
 +  - Durante l'esecuzione del blocco //catch()// vengono disallocate tutte le variabili //locali// del blocco //try// (in ordine inverso al quale erano state allocate) e try si considera concluso. 
 +  - Per il punto precedente, quindi, il parametro passato, non dovrebbe essere un puntatore ad una variabile locale <del>di try</del>, ma al massimo un puntatore a una variabile allocata con //new//, ricordando di disallocarla con //delete// dentro //catch()//.
 +  - Sempre per la stessa ragione, un distruttore non deve mai generare un'eccezione, perché le eccezioni non si possono annidare, si possono gestire solo una alla volta. In caso contrario si chiuderebbe immediatamente il programma.
 +  - Il passaggio di parametri avviene sempre per copia quindi potrebbe essere invocato un costruttore di copia, a sua volta possibile fonte di ulteriori eccezioni, che non si possono annidare... Quindi è meglio usare i //const reference//.
 +  - Al termine di un try-senza-catch o di un try-con-catch, l'esecuzione riprende subito dopo l'ultimo catch.
 +
 +{{:appunti3s:throw.png|}} {{ :appunti3s:catch.png|}}
 +
 +===== Re-throw ===== 
 +Il blocchi //try// possono a loro volta contenere altri blocchi //try// all'interno. In C++ l'ambiente di esecuzione run time assicura anche la presenza di un blocco esterno //try// "globale". Se l'eccezione, lanciata con //throw();//, non viene catturata da nessun //catch// "locale" (seguente il //try//), questa verrà rilanciata verso il blocco //try// esterno (globale), causando la chiusura del programma senza deallocazione della memoria. Si può comunque chiudere il main dentro un //try-catch// e al suo interno usare altri //try-catch//. Se __dentro__ un //catch// "interno" uso //throw;//, senza parentesi, l'eccezione viene rilanciata ad un //catch// "esterno".  
 +
 +{{:appunti3s:throw2.png?|}}  
 +
 +Se non viene trovato nessun catch che gestisce l'eccezione il programma si chiuderà, distruggendo  (a ritroso) tutti gli oggetti creati dal programma, tranne quelli allocati dinamicamente.
 +===== Throw list =====  
 +Quando una funzione contiene il "lanciatore" //throw();// sarebbe bene scriverlo anche in fondo alla sua dichiarazione. <code>void funzione(tipovar) const throw(tipouno, tipodue); // dichiarazione di funzione costante</code> 
 +Dentro le parentesi di questo throw() va indicata la lista dei tipi di eccezioni che possono essere "lanciate" da questa funzione. Nel caso in cui il programmatore non rispetti questa dichiarazione il programma genererà un errore in run-time. Se tra parentesi non viene indicato nessun parametro, significa che non verrà lanciato nessun tipo di eccezione. Dentro le parentesi di ogni blocco //catch()// sarà indicato il tipo di eccezione che quel blocco sa "ricevere"
 +
 +===== Catch generico ===== 
 +Alla fine della lista dei gestori //catch()// può essere utile inserire un blocco //catch()// che raccolga tutto quello che non è stato ricevuto dai suoi predecessori. Tale gestore deve avere la seguente sintassi: 
 +
 +<code> catch(...)  {    // contenuto }
 +</code> 
 +Tuttavia questo sistema rischia di catturare anche eccezioni impreviste. La cosa migliore è far derivare tutte le eccezioni dalla classe base //std%%::%%exception// e usare questa forma 
 +
 +<code> catch (std::exception& e) {   //contenuto } </code>.
 +
 +Questo catch generico è spesso controproducente perché catturano delle eccezioni che potrebbero essere gestite ad un livello più esterno. Esistono delle eccezioni che non sono di stretta competenza della funzione dove si è generata l'eccezione. Ad esempio, un costruttore fa //new// e viene generata un'eccezione //std%%::%%bad_allocate//, ma la gestione di questo tipo di eccezione è esterna perché è comune a tutte le funzioni.
 +
 +Se //throw;// viene usato nel catch generico, allora quel catch generico potrebbe essere inutile.
 +Scrivere un altro blocco //catch()// dopo il catch generico è inutile perché quest'ultimo rende invisibile il primo. 
 +===== Standard exception=====
 +
 +Che oggetto lanciare come eccezione? Si potrebbe lanciare anche un tipo primitivo (int) ma lanciare un oggetto permette di far passare un numero maggiore di informazioni sull'errore. 
 +Esistono degli appositi oggetti creati per questo scopo. In italiano si potrebbero chiamare come "eccezioni tipiche". Sono oggetti appartenenti alla libreria standard (perciò la parola standard exception). Ogni classe di eccezioni descrive un certo tipo di errore.  
 +Per poterne usufruire si deve:
 +  * includere l' header //<stdexcept>//
 +  * usare namespace //exception// (che è contenuto nel namespace //std//)  
 +
 +Poiché le eccezioni sono delle classi, tra loro esiste una gerarchia e si possono utilizzare tutti gli strumenti offerti dal polimorfismo: la classe base si chiama //std%%::%%exception//, e da essa derivano le seguenti classi:
 +
 +^ header^ classe ^ descrizione ^ 
 +| <new> | std%%::%%bad_alloc | l'operatore new fallisce| 
 +| <exception> | std%%::%%bad_exception | il programmatore non ha rispettato la throw list | 
 +| <typeinfo> | std%%::%%bad_cast | errore nel dinamic cast |  
 +| ??? | std%%::%%logic_error |  classe base |
 +| ??? | std%%::%%runtime_error |  classe base |
 +
 +la classe //std%%::%%logic_error// è a sua volta una classe base da cui derivano le seguenti classi:
 +
 +^ header^ classe ^ descrizione ^ 
 +| ??? | std%%::%%domain_error | errore di operazione matematica | 
 +| ??? | std%%::%%invalid_argument | .... | 
 +| ??? | std%%::%%out_of_range |errore di posizionamento su stringa? | 
 +| ??? | std%%::%%length_error|  ... |
 +
 +la classe //std%%::%%runtime_error// è a sua volta una classe base da cui derivano le seguenti classi:
 +
 +^ header^ classe ^ descrizione ^ 
 +| ??? | std%%::%%range_error | ... | 
 +| ??? | std%%::%%overflow_error |errore di operazione matematica | 
 +| ??? | std%%::%%underflow_error |errore di operazione matematica | 
 + 
 +
 +===== Regole pratiche =====  
 +  - tutti i dati che vengono dall'esterno del programma devono essere considerati "sporchi" e devono essere "ripuliti" (sanitezed) prima di utilizzarli.
 +  - ????meglio non usare //try// per racchiudere la chiamata di una funzione, ma usare //try// nel corpo della funzione da chiamare (altrimenti catch non riuscirebbe a distruggere gli oggetti creati dalla funzione)
 +  - racchiudere //new// dentro //try// (//std%%::%%bad_allocate//)
 +  - se un blocco //try// viene dopo un //new//, allora il relativo //catch()// deve eseguire prima di tutto //delete// (in ordine inverso) e poi rilanciare re-throw (vedere definizione n.7)
 +  - racchiudere //return oggetto;// dentro //try// perché potrebbe generare eccezione di memoria...??? 
 +  - racchiudere le inizializzazioni e le chiamate dei costruttori dentro //try// (vedi regola 0)
 +  - se un blocco //try// __crea__, __copia__, o modifica un oggetto, allora sarebbe stato meglio lavorare su una copia di tale oggetto e solo dopo trasferire i dati all'oggetto originale (swap). 
 +  - se __distrugge__ un oggetto non deve generare mai eccezioni.
 +  - tipi primitivi, references e puntatori non generano eccezioni (esempio???)
 +  - usare per le classi di eccezioni l'ereditarietà virtuale e non l'ereditarietà multipla
 +  - se due funzioni devono essere eseguite in sequenza, eseguire prima la più critica.
 +  - se non si può modificare l'ordine di esecuzione, racchiuderle entrambe dentro //try// (come in una transazione) e aggiungere un //catch()// che ripristina la situazione iniziale (come un rollback).
 +  - usare sempre Resource Aquisition Is Inizialisation ([[appunti3s:RAII]])