indice > linguaggio_c

Gestione delle eccezioni

Quando il programmatore deve scrivere un programma spesso incontra errori di diverso tipo:

  1. errori di compilazione
  2. errori di linking
  3. errori di esecuzione (run-time)
  4. 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.

In C si controllava il buon esito di una funzione controllando il valore restituito (0 = nessun errore). In C si chiamava exit() oppure abort() lasciando eventuali oggetti dinamici allocati in memoria. Per evitare questi problemi e usare un sistema ben organizzato, in C++ è sempre meglio usare le eccezioni.

Indipendentemente dalla strategia usata, il programmatore che vuole minimizzare gli errori, cerca di prevernirli usando strumenti con il debugger e il testing.

Le eccezioni sono oggetti (di tipo classe) che sono generati quando c'è qualcosa che non va, come quando cerco di mettere un tipo di dato in una variabile di un tipo diverso. Questi oggetti possono contenere informazioni riguardanti il tipo di errore. Un errore è gestito con le eccezioni quando quel tipo di errore non è frequente.

Alcune volte il programmatore scrive il codice di nuove funzioni, altre volte usa le funzioni create da qualcun altro.

Usando le eccezioni si possono dividere queste due attività:

  • il programmatore che scrive il codice delle funzioni indica agli altri solo quale tipo di eccezioni vengono lanciate (e quando).
  • il programmatore che usa una funzione scritta da qualcun altro indica solo cosa deve fare il suo programma quando riceve 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 significa anche dover ripercorrere (srotolare) lo stack delle chiamate delle funzioni: un'operazione non velocissima…

  1. Un blocco try racchiude il codice “problematico”.
  2. Il blocco try deve contenere le condizioni (if) per distinguere la presenza di un problema o un errore.
  3. Uno o più blocchi catch(), che seguono il blocco try, racchiudono il codice di “gestione” dell'errore.
  4. Ogni condizione (if) verificata dentro try deve “lanciare” l'eccezione all'esterno di questo blocco, usando throw();, un parametro che identifichi l'errore. Un'altra funzione di throw(); è quella di interrompere il flusso del programma dirigendolo verso catch().
  5. Dentro le parentesi tonde di ogni blocco catch() sarà indicato (come un parametro) il tipo di eccezione che quel blocco sa “ricevere”. esempio
  6. Dopo il blocco try si trovano uno o più blocchi catch() che ricevono il controllo del flusso quando il tipo di parametro che viene loro “lanciato” è loro corrispondente. L'ordine con cui vengono specificati i blocchi catch() è lo stesso in cui verranno eseguiti. esempio…
  7. Durante l'esecuzione del blocco catch() vengono disallocate tutte le variabili locali del blocco try (in ordine inverso al quale erano state allocate).
  8. Per la definizione precedente, quindi, per il passaggio del parametro, non si dovrebbe usare un puntatore ad una variabile locale di try, ma al massimo un puntatore a una variabile allocata con new, ricordando di disallocarla con delete dentro catch().
  9. Sempre per la stessa ragione, il distruttore non deve mai generare un'eccezione, perché le eccezioni non si possono annidare, si possono gestire solo una alla volta. Si chiuderebbe immediatamente il programma.
  10. 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… Meglio usare i const reference.

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”.

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.

Quando una funzione contiene il “lanciatore” throw(); sarebbe bene scriverlo anche in fondo alla sua dichiarazione.

void funzione(tipovar) const throw(tipouno, tipodue); // dichiarazione di funzione costante

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”.

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:

 catch(...)  {    // contenuto }

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

 catch (std%%::%%exception& e) {   //contenuto } 

. 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.

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 polimorfismo
??? std::logic_error ???
??? std::runtime_error ???

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 operazione matematica
??? 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
  1. tutti i dati che vengono dall'esterno del programma devono essere considerati “sporchi” e devono essere “ripuliti” (sanitezed) prima di utilizzarli.
  2. 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)
  3. racchiudere new dentro try (std::bad_allocate)
  4. 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)
  5. racchiudere return oggetto; dentro try perché potrebbe generare eccezione di memoria…???
  6. racchiudere le inizializzazioni e le chiamate dei costruttori dentro try (vedi regola 0)
  7. 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).
  8. se distrugge un oggetto non deve generare mai eccezioni.
  9. tipi primitivi, references e puntatori non generano eccezioni (esempio???)
  10. usare per le classi di eccezioni l'ereditarietà virtuale e non l'ereditarietà multipla
  11. se due funzioni devono essere eseguite in sequenza, eseguire prima la più critica.
  12. se non si può modificare l'ordine di esecuzione, racchiuderle entrambe dentro try (transazione) e aggiungere un catch() che ripristina la situazione iniziale (rollback).
  13. usare sempre Resource Aquisition Is Inizialisation (RAII)
  • appunti3s/gestione_delle_eccezioni.txt
  • Last modified: 2018/05/03 22:30
  • by profpro