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:
- Eseguire eventuali pulizie all'interno del costruttore se non si e` in grado
di terminare correttamente la costruzione dell'oggetto;
- 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.
|