Modalitati clasice de tratare a erorilor
Vom prezenta notiunile legate de tratarea erorilor cu ajutorul unui exemplu, si anume, vom considera o clasa Vector pentru reprezentarea vectorilor de elemente intregi:
| class
Vector {
int *p; int size; static int fan_err; static int eroare; public: Vector(int s); //aloca un tablou de s elemente ~Vector(){if(p) delete p;} int& operator[](int i); //returneaza o referinta la elementul cu //indice i, daca 0<=i<size int find(int v); //returneaza indicele elementului cu valoarea //v, daca el exista int min(int i); //returneaza elem. cu val. min din subtabloul //cuprins intre indicii 0 si i }; int Vector::fan_err=0; int Vector::eroare=0; |
Considerand ca se aplica o tratare
clasica a erorilor, functiile clasei Vector ar putea fi definite ca mai
jos:
| Vector::Vector(int
s){
if((size=s)==0) p=NULL; else p=new int[s]; } int& Vector::operator[](int
i){
int Vector::find(int v){
int Vector::min(int i){
|
O functie confruntata cu o situatie de eroare, in cazul tratarii clasice, poate adopta una din urmatoarele optiuni:
| int
*p=new int[10];
p[20]=6;//rezultatul 'returnat' de aceasta //expresie este 6, dar executia //operatiei corupe starea heap-ului |
Cu privire la alternativa [2] trebuie spus ca ea nu este intotdeauna usor de aplicat. Daca pentru functia Vector::find, de exemplu, valoarea -1 ca indicator de esec este satisfacatoare (este clar ca un indice de tablou nu poate fi negativ in mod normal), in schimb pentru Vector::operator[] si Vector::min ce valoare a tipului int ar putea fi considerata ca "iesita din comun"? Pentru asemenea situatii o solutie ar putea fi aceea ca, in loc de int, tipul returnat sa fie o structura compusa din rezultatul propriu-zis si un fanion de eroare.
Pe de alta parte, chiar si acolo unde este aplicabila, optiunea [2] implica necesitatea testarii de fiecare data a rezultatului executiei functiilor respective. Acest lucru poate duce usor la dublarea dimensiunii codului.
Alternativa [3] prezinta dezavantajul ca apelantul functiei in care a aparut eroarea poate sa nu "observe" ca s-a intamplat ceva. Astfel, multe functii din biblioteca standard C indica aparitia unei erori prin setarea variabilei globale errno. De cele mai multe ori, insa, in programe nu se testeaza valoarea lui errno suficient de sistematic incat sa se obtina o tratare consistenta a erorilor.
Tratarea erorilor cu ajutorul exceptiilor
Vom relua exemplul cu clasa Vector si vom rescrie functia Vector::operator[] astfel incat sa lucreze cu exceptii:
| class
Vector {
//... public: class ErrIndex{}; //... }; int& Vector::operator[](int
i){
//exemplu de utilizare void o_functie(){
//... (*) } |
Mecanismul exceptiilor aduce ca noutate separarea explicita a secventelor de cod "normale" fata de cele de tratare a erorilor. In felul acesta programele devin mult mai clare, simplificandu-se cooperarea intre fragmente de program scrise separat.
Ideea fundamentala care sta la baza mecanismului exceptiilor este aceea ca daca o functie oarecare F1 se confrunta cu o situatie pe care nu o poate rezolva, ea va anunta acest lucru apelantului ei, prin emiterea unei asa-numite exceptii, "sperand" ca apelantul va putea (direct sau indirect) sa rezolve problema. O functie F2 care doreste sa rezolve o situatie de exceptie trebuie sa-si manifeste intentia de a capta exceptia emisa in situatia respectiva. Secventa de program care realizeaza captarea unei exceptii se numeste handler de exceptii.
C++ pune la dispozitie 3 constructii de baza pentru lucrul cu exceptii:
In exemplul propus s-a definit clasa Vector::ErrIndex pentru a reprezenta exceptiile emise la utilizarea unui indice eronat in contextul obiectelor de tip Vector. Se observa ca aceasta clasa nu contine nici un membru si, ca urmare, obiectele ei nu transmit nici o alta informatie decat simplul fapt ca, la o referire de forma v[i], unde v este de tip Vector, s-a utilizat o valoare eronata pentru i.
Instructiunea throw
Ca sintaxa, aceasta intructiune este
asemanatoare cu return:
throw expresie_exceptie;
unde expresie_exceptie are ca rezultat un obiect apartinand unui tip ales de proiectant pentru reprezentarea exceptiilor.
Obs: exista si varianta simpla, fara argument a instructiunii throw. Rolul ei este de a retransmite o exceptie si va fi discutat in paragraful referitor la blocurile catch.
Presupunem pentru inceput ca instructiunea throw nu apare in interiorul unui bloc try. In acest caz putem spune ca throw si return sunt asemanatoare si d.p.d.v. al comportamentului, in sensul ca executia lui throw inseamna, printre altele:
Intre instructiunile throw si return exista si deosebiri:
Sintaxa limbajului C++ permite specificarea
in prototipul unei functii tipurile de exceptii pe care aceasta le poate
emite, prin adaugarea unei clauze throw:
tip_rez func(lista_par_form) throw(tip_exp1,. . .,tip_expn);
Clauza throw poate lipsi sau poate avea lista de tipuri de exceptii vida. Vom avea deci pentru specificarea unei functii urmatoarele posibilitati:
Clauza throw face parte din interfata (prototipul) unei functii. Prezenta ei aduce un plus de claritate pentru utilizatorii functiei respective, mai ales atunci cand ei nu au acces la definitia functiei.
Din punct de vedere sintactic un bloc try-catch are forma:
| try
{
//secventa obisnuita de operatii, care poate fi //intrerupta de aparitia unei exceptii } catch(tip_ex1 e){ //secventa tratare exceptie de tip tip_ex1 } catch(tip_ex2 e){ //secventa tratare exceptie de tip tip_ex2 } //. . . catch(tip_exn e){
//. . . (*) |
Un bloc try poate fi urmat de unul sau mai multe blocuri catch. Modul de functionare al unei asemenea constructii este urmatorul: daca in decursul desfasurarii secventei de operatii din blocul try este emisa o exceptie, fie direct (adica prin executia unui throw explicit), fie indirect (adica prin apelul unei functii care emite exceptia), secventa se intrerupe si se baleiaza lista de blocuri catch asociate pentru a-l detecta pe cel corespunzator tipului de exceptie emisa. Daca un astfel de bloc exista, se va lansa in executie secventa de tratare a exceptiei respective. Daca aceasta secventa nu trece printr-un return sau throw, la terminarea ei executia programului continua cu linia care urmeaza dupa ultimul bloc catch atasat blocului try curent (cea notata cu (*) in figura de mai sus).
Trebuie precizat ca un bloc catch poate avea si forma:
| catch(.
. .){
//secventa tratare exceptie de orice tip } |
Un astfel de bloc capteaza exceptii de orice tip: este handler universal.
Daca nu exista nici un bloc catch corespunzator exceptiei emise, aceasta va fi propagata spre eventuale blocuri try-catch exterioare.
Este posibil ca o secventa catch sa execute la randul ei o comanda throw. Efectul in acest caz este intreruperea functiei curente prin emisia unei exceptii care va fi preluata de unul din apelantii functiei. Exceptia emisa dintr-un bloc catch poate fi aceeasi cu cea primita spre rezolvare (spunem ca are loc restransmisia exceptiei), sau o alta noua. Retransmisia unei exceptii se face cu ajutorul unei instructiuni throw fara argument.
Practic, un bloc catch poate fi asimilat cu o functie care are un singur parametru, de tipul celui specificat dupa cuvantul catch. Apelul unei asemenea functii se face automat, la aparitia unei exceptii de tip corespunzator in blocul try la care este atasat blocul catch respectiv.
Referitor la "drumul" parcurs de o exceptie din momentul emiterii sale si pana ajunge sa fie tratata, lucrurile stau astfel:
Cu ajutorul unor mici exemple vom ilustra mecanismul de functionare a blocurilor try-catch si a instructiunii throw.
Presupunem ca la un moment dat lantul
de apeluri este main -> si_inca_una
-> alta_functie -> o_functie
si ca o_functie executa instructiunea
throw. Se creaza o exceptie de tip O_exceptie,
se iese din o_functie, revenindu-se
in alta_functie. Aici se constata
ca nu exista bloc try si ca urmare se iese
din alta_functie ca si cum aici
ar exista un throw, si se revine in si_inca_una.
De data aceasta revenirea are loc in interiorul unui bloc try
(cel
notat cu 1). Se cauta printre ramurile sale catch
si se gaseste cea cu parametru de tip O_exceptie.
Daca exceptia se poate rezolva, dupa executia secventei catch
programul continua cu blocul try urmator (cel
notat cu 2).
Daca se ajunge la retransmisia
exceptiei, atunci se iese din si_inca_una
si se verifica daca apelul la aceasta functie nu se afla in interiorul
unui bloc try din apelant (main,
adica). Atentie deci: la executia unui throw din
interiorul unui bloc catch nu se rebaleiaza lista curenta de handler-e
si nici nu se trece la un bloc try-catch aflat "mai la vale" in
aceeasi functie, chiar daca acesta din urma ar putea capta exceptia emisa
(cum ar fi cazul blocului try 2 din exemplul de mai sus).
Exista si posibilitatea ca un bloc
try-catch
sa fie inclus in secventa try a unui alt bloc
try-catch,
ca in urmatorul exemplu:
| class
O_exceptie {/*...*/};
class Alta_exceptie {/*...*/}; void o_functie(){
void alta_functie(){
//. . . }//end alta_functie() |
In exemplul prezentat se observa ca
blocul try-catch notat cu 2 nu va capta exceptii
de tipul O_exceptie. Blocul
try-catch
notat cu 1 este considerat ca inconjurator pentru blocul 2 si deci, exceptia
emisa de o_functie va ajunge
sa fie tratata de handler-ul corespunzator atasat blocului 1.
In concluzie,
se considera ca un bloc try este inconjurator pentru un throw daca secventa
try
Blocurile try si catch reprezinta si domenii de definitii de nume. Cu alte cuvinte, identificatorii definiti in interiorul acestor blocuri sunt vizibile si traiesc doar acolo. Exemplu:
|
void o_functie(){ int i1; //. . . try{ int i2; //. . . } catch(O_exceptie e){ int i3; //. . . } catch(Alta_exceptie e){ i1=1; //OK i2=2; //eroare, i2 nu e vizibil i3=3; //eroare, i3 nu e vizibil //... } //. . . }//end o_functie() |
Gruparea exceptiilor
Este posibil ca proiectantul sa-si defineasca o ierarhie de tipuri de exceptie. In acest caz unul si acelasi handler poate capta exceptii apartinand unor tipuri aflate in relatia clasa de baza-clasa derivata:
| class
MathErr {/*...*/};
class Overflow: public MathErr
{/*...*/};
void o_functie(){ //. . .
//. . . }
|
Atunci cand nu se doreste tratarea diferentiata a 2 tipuri de exceptii care deriva dintr-o baza comuna, se va scrie un handler care va avea ca parametru o exceptie de tipul clasei de baza. In acest context se pot exploata binefacerile mecanismului de functii virtuale, cu conditia ca tipul parametrului handler-ului sa fie referinta sau adresa la clasa de baza respectiva (cum ar fi MathErr& in exemplul de mai sus):
| class
MathErr {
//... public: virtual void print_mes(); //... }; class Overflow: public MathErr
{/*...*/};
void o_functie(){ //. . .
}
|
Un lucru important care trebuie specificat
in legatura cu gruparea exceptiilor este acela ca daca intr-un bloc try-catch
exista cel putin 2 handler-e pentru care tipurile exceptiilor sunt in relatia
supertip-subtip sau exista handler-ul universal, atunci nu este indiferent
in ce ordine plasam in program handler-ele respective. Aceasta restrictie
deriva din faptul ca, la emiterea unei exceptii, cautarea unui catch
corespunzator se face parcurgand lista de blocuri catch
in ordinea textuala in care apar ele.
Regula generala
este ca handler-ul corespunzator subtipului sa se scrie inaintea celui
corespunzator supertipului (asa cum se vede in exemplul de mai sus), iar
handler-ul universal, daca exista, sa fie ultimul din lista. Daca
nu se respecta aceasta regula, se ajunge in situatia ca anumite handler-e
nu vor fi niciodata accesate, ele fiind "mascate" de handler-ele situate
inaintea lor si care vor capta toate exceptiile aparute.
Constructorii/distructorii
in contextul tratarii exceptiilor
Intr-un paragraf anterior se facea
afirmatia ca terminarea prin throw a unei
functii este considerata ca "anormala" si ca lucrul acesta este semnificativ
din punct de vedere al mecanismului de functionare a constructorilor/distructorilor.
Sa lamurim ce inseamna aceasta: daca
la crearea unui obiect constructorul implicat se termina printr-un throw,
la disparitia obiectului respectiv NU
se va mai executa distructorul, deoarece se considera ca obiectul nu a
fost initializat complet.
Pe de alta parte insa (si aici e salvarea),
daca intre membrii obiectului se afla alte obiecte si acestea au apucat
sa fie initializate corect, pentru ele se vor executa distructorii aferenti.
Consecintele acestui mod de functionare
se "simt" atunci cand un constructor apuca sa realizeze alocarea uneia
sau a mai multor resurse (de ex. alocari din heap sau deschideri de fisiere),
dupa care, dintr-un motiv oarecare, se termina prin throw.
Distructorul, care ar trebui in mod normal sa elibereze resursele respective,
nu se va executa la disparitia obiectului, deci resursele raman alocate.
Exemplu:
| class
o_clasa{
int *p; int size; public: o_clasa(int s){p=new int[size=s]; init(p);} void init(int *pp){ //... if(eroare) throw Exceptie(); } ~o_clasa(){delete[] p;} }; void o_functie(){
}
|
Portita de scapare pentru aceste situatii
consta in a realiza alocarea de resurse prin intermediul unor obiecte,
asa ca in urmatoarea secventa:
| class
MemPtr{
int *p; public: MemPtr(int s){p=new int[s];} ~MemPtr(){delete[] p;} operator int*(){return p;} //acest operator asigura utilizarea obiectelor de tip MemPtr ca si //cum ar fi de tip int* }; class o_clasa{ MemPtr ob_p; int size; public: o_clasa(int s):p(s),size(s){init(ob_p);} void init(int *pp){ //... if(eroare) throw Exceptie(); } ~o_clasa(){} }; void o_functie(){
//. . . }
|
Politica aplicata prin solutia de mai
sus este cunoscuta sub denumirea de "alocarea resurselor
prin initializare".
Acest concept poate fi sintetizat astfel: fie o clasa de forma data mai jos:
| class o_clasa{
tip_1 resursa_1; tip_2 resursa_2; //. . . tip_n resursa_n; public: o_clasa(/*parametri*/){ alocare_resursa_1(); alocare_resursa_2(); //. . . alocare_resursa_n(); //. . . } ~o_clasa(){ eliberare_resursa_1(); eliberare_resursa_2(); //. . . eliberare_resursa_n(); //. . . } //. . . }; |
Presupunem ca la crearea unui obiect al acestei clase constructorul reuseste sa execute corect alocarea a k resurse (1<= k <n), dupa care esueaza la tentativa de alocare a resursei k+1. In acest caz, la disparitia obiectului respectiv nu se va mai executa distructorul, deci cele k resurse raman alocate. Pentru a rezolva problema trebuie ca tipurile tip_i sa fie clase, constituite dupa modelul clasei MemPtr. Atunci, desi distructorul ~o_clasa nu se executa, totusi distructorii obiectelor resursa_i care au fost create corect se vor executa, ceea ce asigura eliberarea resurselor respective. Astfel se explica denumirea de "alocare prin initializare", si anume alocarea unei resurse se realizeaza de fapt prin initializarea unui obiect resursa_i:
| class o_clasa{
clasa_tip_1 resursa_1; clasa_tip_2 resursa_2; //. . . clasa_tip_n resursa_n; public: o_clasa(/*parametri*/): resursa_1(/*par1*/), resursa_2(/*par2*/), //. . . resursa_n(/*parn*/){ //. . . } ~o_clasa(){ //. . . } //. . . }; class clasa_tip_i{ tip_i resursa_i; public: clasa_tip_i(/*parametri*/):{ //. . . } ~clasa_tip_i(){ //. . . } operator tip_i(){ return resursa_i; } //. . . }; |
Tema
1.Se cere sa se definesca o clasa
Queue
care modeleaza functionarea unei cozi FIFO cu elemente de tip char.
Dimensiunea cozii este limitata si se specifica la creare. Operatiile care
se pot aplica asupra cozii sunt:
2.Se cere sa se defineasca o clasa
Files
care sa modeleze lucrul cu fisiere de text, folosindu-se tipul clasic FILE*.
Constructorul clasei va realiza deschiderea fisierului, iar distructorul
va realiza inchiderea. Problema va fi prezentata in 2 variante: cu si fara
aplicarea principiului de "alocare prin initializare", urmarindu-se modul
de executie a constructorilor si distructorilor. In constructor se va simula
o situatie de eroare oarecare dupa ce s-a deschis fisierul.
Ca metode pentru clasa Files,
se vor prevedea minimum:
Bibliografie
[Bol98] W.R. Bolton - "Free Course Pamphlets", http://www.mindspring.com/~wrba/frepam/index.htm (legatura pe 'CPPTHROW')
[Gre99] C. Green - "Cliff's Teaching Info and Technical Resources", http://www.halcyon.com/cliffg/uwteach/index.html (legatura pe 'C++ Programming: Intermediate')
[Str91] B. Stroustrup - "The
C++ programming language", AT&T, 1991