Le eccezioni


Durante l'esecuzione di un applicativo possono verificarsi delle situazioni di errore non verificabili a compile-time, che in qualche modo vanno gestiti.
Le possibili tipologie di errori sono diverse ed in generale non tutte trattabili allo stesso modo. In particolare possiamo distinguere tra errori che non compromettono il funzionamento del programma ed errori che invece costituiscono una grave impedimento al normale svolgimento delle operazioni.
Tipico della prima categoria sono ad esempio gli errori dovuti a errato input dell'utente, facili da gestire senza grossi problemi. Meno facili da catturare e gestire e` invece la seconda categoria cui possiamo inserire ad esempio i fallimenti relativi all'acquisizione di risorse come la memoria dinamica; questo genere di errori viene solitamente indicato con il termine di eccezioni per sottolineare la loro caratteristica di essere eventi particolarmente rari e di comportare il fallimento di tutta una sequenza di operazioni.
La principale difficolta` connessa al trattamento delle eccezioni e` quella di riportare lo stato dell'applicazione ad un valore consistente. Il verificarsi di un tale vento comporta infatti (in linea di principio) l'interruzione di tutta una sequenza di operazioni rivolta ad assolvere ad una certa funzionalita`, allo svuotamento dello stack ed alla deallocazione di eventuali risorse allocate fino a quel punto relativamente alla richiesta in esecuzione. Le informazioni necessarie allo svolgimento di queste operazioni sono in generale dipendenti anche dal momento e dal punto in cui si verifica l'eccezione e non e` quindi immaginabile (o comunque facile) che la gestione dell'errore possa essere incapsulata in un unico blocco di codice richiamabile indipendentemente dal contesto in cui si verifica il problema.
In linguaggi che non offrono alcun supporto, catturare e gestire questi errori puo` essere particolarmente costoso e difficile, al punto che spesso si rinuncia lasciando sostanzialmente al caso le conseguenze. Il C++ comunque non rientra tra questi linguaggi e offre alcuni strumenti che saranno oggetto dei successivi paragrafi di questo capitolo.



Segnalare le eccezioni

Il primo problema che bisogna affrontare quando si verifica un errore e` capire dove e quando bisogna gestire l'anomalia.
Poniamo il caso che si stia sviluppando una serie di funzioni matematiche, in particolare una che esegue la radice quadrata. Come comportarsi se l'argomento della funzione e` un numero negativo? Le possibilita` sono due:

  • Terminare il processo;
  • Segnalare l'errore al chiamante.
Probabilmente la prima possibilita` e` eccessivamente drastica, tanto piu` che non sappiamo a priori se e` il caso di terminare oppure se il chiamante possa prevedere azioni alternative da compiere in caso di errore (ad esempio ignorare l'operazione e passare alla successiva). D'altronde neanche la seconda possibilita` sarebbe di per se` una buona soluzione, cosa succede se il chiamante ignora l'errore proseguendo come se nulla fosse?
E` chiaramente necessario un meccanismo che garantisca che nel caso il chiamante non catturi l'anomalia qualcuno intervenga in qualche modo.
Ma andiamo con ordine, e supponiamo che il chiamante preveda del codice per gestire l'anomalia.
Se al verificarsi di un errore grave non si dispone delle informazioni necessarie per decidere cosa fare, la cosa migliore da farsi e` segnalare la condizione di errore a colui che ha invocato l'operazione. Questo obiettivo viene raggiunto con la keyword throw:


  int Divide(int a, int b) {
    if (b) return a/b;
    throw "Divisione per zero";
  }


L'esecuzione di throw provoca l'uscita dal blocco in cui essa si trova (si noti che in questo caso la funzione non e` obbligata a restituire alcun valore tramite return) e in queste situazioni si dice che la funzione ha sollevato (o lanciato) una eccezione.
La throw accetta un argomento come parametro che viene utilizzato per creare un oggetto che non ubbidisce alle normali regole di scope e che viene restituito a chi ha tentato l'esecuzione dell'operazione (nel nostro caso al blocco in cui Divide e` stata chiamata). Il compito di questo oggetto e` trasportare tutte le informazioni utili sull'evento.
L'argomento di throw puo` essere sia un tipo predefinito che un tipo definito dal programmatore.
Per compatibilita` con il vecchio codice, una funzione non e` tenuta a segnalare la possibilita` che possa lanciare una eccezione, ma e` buona norma avvisare dell'eventualita` segnalando quali tipologie di eccezioni sono possibili. Allo scopo si usa ancora throw seguita da una coppia di parentesi tonde contenente la lista dei tipi di eccezione che possono essere sollevate:


  int Divide(int a, int b) throw(char*) {
    if (b) return a/b;
    throw "Errore";
  }

  void MoreExceptionTypes() throw(int, float, MyClass&) {
    /* ... */
  }


Nel caso della Divide si segnala la possibilita` che venga sollevata una eccezione di tipo char*; nel caso della seconda funzione invece a seconda dei casi puo` essere lanciata una eccezione di tipo int, oppure di tipo float, oppure ancora una di tipo MyClass& (supponendo che MyClass sia un tipo precedentemente definito).



Gestire le eccezioni

Quanto abbiamo visto chiaramente non e` sufficiente, non basta poter sollevare (segnalare) una eccezione ma e` necessario poterla anche catturare e gestire.
L'intenzione di catturare e gestire l'eventuale eccezione deve essere segnalata al compilatore utilizzando un blocco try:


  #include < iostream >
  using namespace std;

  int Divide(int a, int b) throw(char*) {
    if (b) return a/b;
    throw "Errore";
  }

  int main() {
    cout << "Immettere il dividendo: ";
    int a;
    cin >> a;
    cout << endl << "Immettere il divisore: ";
    int b;
    cin >> b;
    try {
      cout << Divide(a, b);
    }
    /* ... */
  }


Utilizzando try e racchidendo tra parentesi graffe (le parentesi si devono utilizzate sempre) il codice che puo` generare una eccezione si segnala al compilatore che siamo pronti a gestire l'eventuale eccezione.
Ci si potra` chiedere per quale motivo sia necessario informare il compilatore dell'intenzione di catturare e gestire l'eccezione, il motivo sara` chiaro in seguito, al momento e` sufficiente sapere che cio` ha il compito di indicare quando certi automatismi dovranno arrestarsi e lasciare il controllo a codice ad hoc preposto alle azioni del caso.
Il codice in questione dovra` essere racchiuso all'interno di un blocco catch che deve seguire il blocco try:

  
  #include < iostream >
  using namespace std;

  int Divide(int a, int b) throw(char*) {
    if (b) return a/b;
    throw "Errore, divisione per 0";
  }

  int main() {
    cout << "Immettere il dividendo: ";
    int a;
    cin >> a;
    cout << endl << "Immettere il divisore: ";
    int b;
    cin >> b;
    cout << endl;
    try {
      cout << "Il risultato e` " << Divide(a, b);
    }
    catch(char* String) {
      cout << String << endl;
      return -1;
    }
    return 0;  
  }


Il generico blocco catch potra` gestire in generale solo una categoria di eccezioni o una eccezione generica. Per fornire codice diverso per diverse tipologie di errori bisognera` utilizzare piu` blocchi catch:

  
   try {
      /* ... */
   }
   catch(Type1 Id1) {
     /* ... */
   }
   catch(Type2 Id2) {
     /* ... */
   }

   /* Altre catch */

   catch(TypeN IdN) {
     /* ... */
   }

   /* Altro */


Ciascuna catch e` detta exception handler e riceve un parametro che e` il tipo di eccezione che viene gestito in quel blocco. Nel caso generale un blocco try sara` seguito da piu` blocchi catch, uno per ogni tipo di eccezione possibile all'interno di quel try. Si noti che le catch devono seguire immediatamente il blocco try.

Quando viene generata una eccezione (throw) il controllo risale indietro fino al primo blocco try. Gli oggetti staticamente allocati (che cioe` sono memorizzati sullo stack) fino a quel momento nei blocchi da cui si esce vengono distrutti invocando il loro distruttore (se esiste). Nel momento in cui si giunge ad un blocco try anche gli oggetti staticamente allocati fino a quel momento dentro il blocco try vengono distrutti ed il controllo passa immediatamente dopo la fine del blocco.
Il tipo dell'oggetto creato con throw viene quindi confrontato con i parametri delle catch che seguono la try. Se viene trovata una catch del tipo corretto, si passa ad eseguire le istruzioni contenute in quel blocco, dopo aver inizializzato il parametro della catch con l'oggetto restituito con throw. Nel momento in cui si entra in un blocco catch, l'eccezione viene considerata gestita ed alla fine del blocco catch il controllo passa alla prima istruzione che segue la lista di catch (sopra indicato con "/* Altro */").
Vediamo un esempio:


  #include < iostream >
  #include < string.h >
  using namespace std;

  class Test {
      char Name[20];
    public:
      Test(char* name){
        Name[0] = '\0';
        strcpy(Name, name);
        cout << "Test constructor inside "
             << Name << endl;
      }
      ~Test() {
        cout << "Test distructor inside "
             << Name << endl;
      }
  };

  int Sub(int b) throw(int) {
    cout << "Sub called" << endl;
    Test k("Sub");
    Test* K2 = new Test("Sub2");
    if (b > 2) return b-2;
    cout << "exception inside Sub..." << endl;
    throw 1;
  }

  int Div(int a, int b) throw(int) {
    cout << "Div called" << endl;
    Test h("Div");
    b = Sub(b);
    Test h2("Div 2");
    if (b) return a/b;
    cout << "exception inside Div..." << endl;
    throw 0;
  }

  int main() {
    try {
      Test g("try");
      int c = Div(10, 2);
      cout << "c = " << c << endl;
    }       // Il controllo ritorna qua
    catch(int exc) {
      cout << "exception catched" << endl;
      cout << "exception value is " << exc << endl;      
    }
    return 0;
  }


La chiamata a Div all'interno della main provoca una eccezione nella Sub, viene quindi distrutto l'oggetto k ed il puntatore k2, ma non l'oggetto puntato (allocato dinamicamente). La deallocazione di oggetti allocati nello heap e` a carico del programmatore.
In seguito alla eccezione, il controllo risale a Div, ma la chiamata a Sub non era racchiusa dentro un blocco try e quindi anche Div viene terminata distruggendo l'oggetto h. L'oggetto h2 non e` stato ancora creato e quindi nessun distruttore per esso viene invocato.
Il controllo e` ora giunto al blocco che ha chiamato la Div, essendo questo un blocco try, vengono distrutti gli oggetti g e c ed il controllo passa nel punto in cui si trova il commento.
A questo punto viene eseguita la catch poiche` il tipo dell'eccezione e` lo stesso del suo argomento e quindi il controllo passa alla return della main.
Ecco l'output del programma:


  Test constructor inside try
  Div called
  Test constructor inside Div
  Sub called
  Test constructor inside Sub
  Test constructor inside Sub 2
  exception inside Sub...
  Test distructor inside Sub
  Test distructor inside Div
  Test distructor inside try
  exception catched
  exception value is 0


Si provi a tracciare l'esecuzione del programma e a ricostruirne la storia, il meccanismo diverra` abbastanza chiaro.

Il compito delle istruzioni contenute nel blocco catch costituiscono quella parte di azioni di recupero che il programma deve svolgere in caso di errore, cosa esattamente mettere in questo blocco e` ovviamente legato alla natura del programma e a cio` che si desidera fare; ad esempio ci potrebbero essere le operazioni per eseguire dell'output su un file di log. E` buona norma studiare gli exception handler in modo che al loro interno non possano verificarsi eccezioni.

Nei casi in cui non interessa distinguere tra piu` tipologie di eccezioni, e` possibile utilizzare un unico blocco catch utilizzando le ellissi:


  try {
    /* ... */
  }
  catch(...) {
    /* ... */
  }


In altri casi invece potrebbe essere necessari passare l'eccezione ad un blocco try ancora piu` esterno, ad esempio perche` a quel livello e` sufficiente (o possibile) fare solo certe operazioni, in questo caso basta utilizzare throw all'interno del blocco catch per reinnescare il meccanismo delle eccezioni a partire da quel punto:


  try {
    /* ... */
  }
  catch(Type Id) {
    /* ... */
    throw;    // Bisogna scrivere solo throw
  }


In questo modo si puo` portare a conoscenza dei blocchi piu` esterni della condizione di errore.



Casi particolari

Esistono ancora due problemi da affrontare

  • Cosa fare se una funzione solleva una eccezione non specificata tra quelle possibili;
  • Cosa fare se non si riesce a trovare una exception handler compatibile con l'eccezione sollevata;
Esaminiamo il primo punto.
Per default una funzione che non specifica una lista di possibili tipi di eccezione puo` sollevare una eccezione di qualsiasi tipo, ma funzione che specifica una lista dei possibili tipi di eccezione e` tenuta a rispettare tale lista. Nel caso non lo facesse, in seguito ad una throw di tipo non previsto, verrebbe eseguita immediatamente la funzione predefinita unexpected(). Per default unexpected() chiama terminate() provocando la terminazione del programma. Questo comportamento puo` pero` essere alterato definendo una nuova funzione da sostituire a unexpected(), questa funzione non deve riceve alcun parametro e deve restituisce void. La nuova funzione va attivata utilizzando set_unexpected() come mostrato nel seguente esempio:


  #include < exception >
  using namespace std;

  void MyUnexpected() {
    /* ... */
  }

  typedef void (* OldUnexpectedPtr) ();

  int main() {
    OldUnexpectedPtr = set_unexpected(MyUnexpected);
    /* ... */
    return 0;
  }


unexpected() e set_unexpected() sono dichiarate nell'header < exception >. E` importante ricordare che la vostra unexpected non deve ritornare, in altre parole deve terminare l'esecuzione del programma:


  #include < exception >
  #include < stdlib.h >
  using namespace std;

  void MyUnexpected() {
    /* ... */
    abort();     // Termina il programma
  }

  typedef void (* OldHandlerPtr) ();

  int main() {
    OldhandlerPtr = set_unexpected(MyUnexpected);
    /* ... */
    return 0;
  }


Il modo in cui terminate l'esecuzione non e` importante, quello che conta e` che la funzione non ritorni.
set_unexpected() infine restituisce l'indirizzo della unexpected precedentemente installata e che in talune occasioni potrebbe servire.

Rimane da trattare il caso in cui in seguito ad una eccezione, non si trova un handler idoneo...I casi possibili sono due:

  • l'eccezione viene lanciata in un blocco di codice tale che risalendo all'indietro non si trova un blocco try;
  • si trova il blocco try ma non si trova una catch compatibile.
Il comportamento sempre seguito dal compilatore e` quello di risalire all'indietro fino a trovare un blocco try con una catch compatibile con l'eccezione. Se non si trova la catch il compilatore continua a risalire, alla fine o si trova la catch oppure si arriva al punto in cui non ci sono piu` blocchi try. Se nessun blocco try viene trovato, viene chiamata la funzione terminate().
Anche in questo caso, come per unexpected(), terminate() e` implementata tramite puntatore ed e` possibile alterarne il funzionamento utilizzando set_terminate() in modo analogo a quanto visto per unexpected() e set_unexpected() (ovviamente la nuova terminate non deve ritornare).
set_terminate() restituisce l'indirizzo della terminate() precedentemente installata.
Al solito la funzione che sostituisce terminate non deve ricevere parametri, deve restituire void e deve terminare l'esecuzione del programma.



Eccezioni e costruttori

Il meccanismo di stack unwinding (srotolamento dello stack) che si innesca quando viene sollevata una eccezione garantisce che gli oggetti allocati sullo stack vengano distrutti via via che il controllo esce dai vari blocchi applicativi.
Ma cosa succede se l'eccezione viene sollevata nel corso dell'esecuzione di un costruttore? In tal caso l'oggetto non puo` essere considerato completamente costruito ed il compilatore non esegue la chiamata al suo distruttore, viene comunque eseguita la chiamata dei distruttori per le componenti dell'oggetto che sono state create:


  #include < iostream >
  using namespace std;

  class Component {
    public:
      Component() {
        cout << "Component constructor called..." << endl;
      }
      ~Component() {
        cout << "Component distructor called..." << endl;
      }
  };

  class Composed {
    private:
      Component A;

    public:
      Composed() {
        cout << "Composed constructor called..." << endl;
        cout << "Throwing an exception..." << endl;
        throw 10;
      }
      ~Composed() {
        cout << "Composed distructor called..." << endl;
      }
  };

  int main() {
    try {
      Composed B;
    }
    catch (int) {
      cout << "Exception handled!" << endl;
    };
    return 0;
  }


Dall'output di questo programma:


  Component constructor called...
  Composed constructor called...
  Throwing an exception...
  Component distructor called...
  Exception handled! 


e` possibile osservare che il distruttore per l'oggetto B istanza di Composed non viene eseguito perche` solo al termine del costruttore tale oggetto puo` essere considerato totalmente realizzato.

Le conseguenze di questo comportamento possono passare inosservate, ma e` importante tenere presente che eventuali risorse allocate nel corpo del costruttore non possono essere deallocate dal distruttore. Bisogna realizzare con cura il costruttore assicurandosi che risorse allocate prima dell'eccezione vengano opportunamente deallocate:


  #include < iostream >
  using namespace std;

  int Divide(int a, int b) throw(int) {
    if (b) return a/b;
    cout << endl;
    cout << "Divide: throwing an exception..." << endl;
    cout << endl;
    throw 10;
  }

  class Component {
    public:
      Component() {
        cout << "Component constructor called..." << endl;
      }
      ~Component() {
        cout << "Component distructor called..." << endl;
      }
  };

  class Composed {
    private:
      Component A;
      float* FloatArray;
      int AnInt;
    public:
      Composed() {
        cout << "Composed constructor called..." << endl;
        FloatArray = new float[10];
        try {
          AnInt = Divide(10,0);
        }
        catch(int) {
          cout << "Exception in Composed constructor...";
          cout << endl << "Cleaning up..." << endl;
          delete[] FloatArray;
          cout << "Rethrowing exception..." << endl;
          cout << endl;
          throw;
        }
      }
      ~Composed() {
        cout << "Composed distructor called..." << endl;
        delete[] FloatArray;
      }
  };

  int main() {
    try {
      Composed B;
    }
    catch (int) {
      cout << "main: exception handled!" << endl;
    };
    return 0;
  }


All'interno del costruttore di Composed viene sollevata una eccezione. Quando questo evento si verifica, il costruttore ha gia` allocato delle risorse (nel nostro caso della memoria); poiche` il distruttore non verrebbe eseguito e` necessario provvedere alla deallocazione di tale risorsa. Per raggiungere tale scopo, le operazioni soggette a potenziale fallimento vengono racchiuse in una try seguita dall'opportuna catch. Nel exception handler tale risorsa viene deallocata e l'eccezione viene nuovamente propagata per consentire alla main di intraprendere ulteriori azioni.
Ecco l'output del programma:


  Component constructor called...
  Composed constructor called...

  Divide: throwing an exception...

  Exception in Composed constructor...
  Cleaning up...
  Rethrowing exception...

  Component distructor called...
  main: exception handled!


Si noti che se la catch del costruttore della classe Composed non avesse rilanciato l'eccezione, il compilatore considerando gestita l'eccezione, avrebbe terminato l'esecuzione del costruttore considerando B completamente costruito. Cio` avrebbe comportato la chiamata del distruttore al termine dell'esecuzione della main con il conseguente errore dovuto al tentativo di rilasciare nuovamente la memoria allocata per FloatArray.
Per verificare cio` si modifichi il programma nel seguente modo:


  #include < iostream >
  using namespace std;

  int Divide(int a, int b) throw(int) {
    if (b) return a/b;
    cout << endl;
    cout << "Divide: throwing an exception..." << endl;
    cout << endl;
    throw 10;
  }

  class Component {
    public:
      Component() {
        cout << "Component constructor called..." << endl;
      }
      ~Component() {
        cout << "Component distructor called..." << endl;
      }
  };

  class Composed {
    private:
      Component A;
      float* FloatArray;
      int AnInt;
    public:
      Composed() {
        cout << "Composed constructor called..." << endl;
        FloatArray = new float[10];
        try {
          AnInt = Divide(10,0);
        }
        catch(int) {
          cout << "Exception in Composed constructor...";
          cout << endl << "Cleaning up..." << endl;
          delete[] FloatArray;
        }
      }
      ~Composed() {
        cout << "Composed distructor called..." << endl;
      }
  };

  int main() {
    try {
      Composed B;
      cout << endl << "main: no exception here!" << endl;
    }
    catch (int) {
      cout << endl << "main: Exception handled!" << endl;
    };
  }


eseguendolo otterrete il seguente output:


  Component constructor called...
  Composed constructor called...

  Divide: throwing an exception...

  Exception in Composed constructor...
  Cleaning up...

  main: no exception here!
  Composed distructor called...
  Component distructor called...


Come si potra` osservare, il blocco try della main viene eseguito normalmente e l'oggetto B viene distrutto non in seguito all'eccezione, ma solo perche` si esce dallo scope del blocco try cui appartiene.

La realizzazione di un costruttore nella cui esecuzione puo` verificarsi una eccezione, e` dunque un compito non banale e in generale sono richieste due operazioni:

  1. Eseguire eventuali pulizie all'interno del costruttore se non si e` in grado di terminare correttamente la costruzione dell'oggetto;
  2. Se il distruttore non termina correttamente (ovvero l'oggetto non viene totalmente costruito), propagare una eccezione anche al codice che ha invocato il costruttore e che altrimenti rischierebbe di utilizzare un oggetto non correttamente creato.


La gerarchia exception

Lo standard prevede tutta una serie di eccezioni, ad esempio l'operatore ::new puo` sollevare una eccezione di tipo bad_alloc, alcune classi standard (ad esempio la gerarchia degli iostream) ne prevedono altre. E` stata prevista anche una serie di classi da utilizzare all'interno dei propri programmi, in particolare in fase di debugging.
Alla base della gerarchia si trova la classe exception da cui derivano logic_error e runtime_error. La classe base attraverso il metodo virtuale what() (che restituisce un char*) e` in grado di fornire una descrizione dell'evento (cosa esattamente c'e` scritto nell'area puntata dal puntatore restituito dipende dall'implementazione).
Le differenze tra logic_error e runtime_error sono sostanzialmente astratte, la prima classe e` stata pensata per segnalare errori logici rilevabili attraverso le precondizioni o le invarianti, la classe runtime_error ha invece lo scopo di riportare errori rilevabili solo a tempo di esecuzione.
Da logic_error e runtime_error derivano poi altre classi secondo il seguente schema:




Le varie classi sostanzialmente differiscono solo concettualmente, non ci sono differenze nel loro codice, in questo caso la derivazione e` stata utilizzata al solo scopo di sottolineare delle differenze di ruolo forse non immediate da capire.
Dallo standard:

  • logic_error ha lo scopo di segnalare un errore che presumibilmente potrebbe essere rilevato prima di eseguire il programma stesso, come la violazione di una precondizione;
  • domain_error va lanciata per segnalare errori relativi alla violazione del dominio;
  • invalid_argument va utilizzato per segnalare il passaggio di un argomento non valido;
  • length_error segnala il tentativo di costruire un oggetto di dimensioni superiori a quelle permesse;
  • out_of_range riporta un errore di argomento con valore non appartenente all'intervallo di definizione;
  • runtime_error rappresenta un errore che puo` essere rilevato solo a runtime;
  • range_error segnala un errore di intervallo durante una computazione interna;
  • overflow_error riporta un errore di overflow;
  • underflow_error segnala una condizione di underflow;
Eccetto che per la classe base che non e` stata pensata per essere impiegata direttamente, il costruttore di tutte le altre classi riceve come unico argomento un const string&; il tipo string e` definito nella libreria standard del linguaggio.



Conclusioni

I meccanismi che sono stati descritti nei paragrafi precedenti costituiscono un potente mezzo per affrontare e risolvere situazioni altrimenti difficilmente trattabili. Non si tratta comunque di uno strumento facile da capire e utilizzare ed e` raccomandabile fare diverse prove ed esercizi per comprendere cio` che sta dietro le quinte. La principale difficolta` e` quella di riconoscere i contesti in cui bisogna utilizzare le eccezioni ed ovviamente la strategia da seguire per gestire tali eventi. Cio` che bisogna tener presente e` che il meccanismo delle eccezioni e` sostanzialmente non locale poiche` il controllo ritorna indietro risalendo i vari blocchi applicativi. Cio` significa che bisogna pensare ad una strategia globale, ma che non tenti di raggruppare tutte le situazioni in un unico errore generico altrimenti si verrebbe schiacciati dalla complessita` del compito.
In generale non e` concepibile occuparsi di una possibile eccezione al livello di ogni singola funzione, a questo livello cio` che e` pensabile fare e` solo lanciare una eccezione; e` invece bene cercare di rendere i propri applicativi molto modulari e isolare e risolvere all'interno di ciascun blocco quante piu` situazioni di errore possibile, lasciando filtrare una eccezione ai livelli superiori solo se le conseguenze possono avere ripercussioni a quei livelli.
Ricordate infine di catturare e trattare le eccezioni standard che si celano dietro ai costrutti predefiniti quali l'operatore globale ::new.



Pagina precedente - Pagina successiva



C++, una panoramica sul linguaggio - seconda edizione
© Copyright 1996-1999, Paolo Marotta