Tomáš Plch / 27. 11. 2009, 00:00
V tomto dieli si povieme viac o tom, čo sa stane ak výnimky neobsluhujeme alebo ich obsluhujeme nesprávne. Vysvetlíme si, ako sa u funkcií správne špecifikuje zoznam vyvolateľných výnimiek.
V predchádzajúcich dieloch sme si povedali o základných pojmoch a ich použití v praxi. V tomto dieli si povieme niečo o udalostiach, ako nesprávne alebo vôbec neriešime výnimky.
V prvom dieli sme si hovorili o troch spôsoboch použitia throw. Teraz si ukážeme poslednú variantu - špecifikovanie zoznamu výnimiek, ktoré funkcia môže vyvolať. A povieme si aj, čo sa stane, ak tento vopred daný zoznam nedodržíme.
Myšlienka stojaca za zoznamom výnimiek je prakticky jednoduchá. Programátor, ktorý používa, či už vlastné alebo cudzie objekty, funkcie, v knižniciach alebo poskytnutých zdrojových súboroch, by rád vedel, aké výnimky môže očakávať. A presne na to je zoznam vyvolaných výnimiek. Na druhú stranu, je daná funkcia limitovaná na špecifickú množinu výnimiek, s ktorými môže operovať.
Špecifikuje sa jednoducho, ale i tu sú určité pravidlá.
void f() throw(int,char);
Táto funkcia môže vyvolať jedine výnimku, ktorá je reprezentovaná typom int alebo typom char. V tomto zozname sa nesmie nachádzať pointer alebo referencia na neúplný typ, prípadne neúplný typ ako taký. Môže sa tu nachádzať pointer na void
, a kvalifikátory const
a volatile
.
V prípade, že tento zoznam neuvediete za funkciou, znamená to, že sú povolené všetky výnimky. Ak je naopak zoznam prázdny, znamená to, že funkcia nevyvolá žiadnu výnimku. Špecifikácia výnimiek nemôže byť obsiahnutá v typdef
deklarácii. Zoznam výnimiek nieje súčasťou deklarácie typu funkcie.
Špecifikácia výnimiek sa môže objaviť ešte v pointeri (ukazovateľ) na funkciu, referencii na funkciu, pointeri na deklaráciu členskej funkcie alebo pointeri na definíciu členskej funkcie.
void f() throw(int); void (*g)() throw(int); void h(void i() throw(int));
Na nasledujúcom príklade si ilustrujeme použitie.
class A { }; class B : public A { }; class C { }; void f(int i) throw (A) { switch (i) { case 0: throw A(); case 1: throw B(); default: throw C(); } } void g(int i) throw (A*) { A* a = new A(); B* b = new B(); C* c = new C(); switch (i) { case 0: throw a; case 1: throw b; default: throw c; } }
V prípade, že dôjde k prípadom 0 a 1, je vyvolaná výnimka v poriadku pretože mechanizmus rešpektuje dedičnosť (táto musí byť verejná). Avšak ak sa program posnaží vyvolať výnimku C* dôjde k problému - a program zavolá funkciu unexpected(). O tej si povieme neskôr.
Ešte si povieme niečo o virtuálnych funkciách. V jednoduchosti, zoznam špecifikovaných výnimiek sa nesmie zväčšovať. Inými slovami, množina špecifikovaných výnimiek vo virtuálnych funkciách musí byť podmnožinou v smere dedičnosti od rodičov k potomkom.
Nasledujúci príklad to pekne ilustruje.
class A { public: virtual void f() throw (int, char); }; class B : public A { public: void f() throw (int, char, double) { } }; class C : public A { public: void f() throw (int ) { } };
Trieda typu B je nesprávne. Trieda C je správne, lebo len zúžila množinu možných výnimiek.
U priraďovania pointerov na funkcie platí nasledujúce pravidlo, ktoré ilustruje príklad:
void (*f)(); void (*g)(); void (*h)() throw (int); void i() { f = h; //1 h = g; //2 }
Prípad 1 je v poriadku, pretože sme len viac obmedzili daný pointer, ktorý ukazoval na funkciu, ktorá dovolila vyvolať ľubovolnú výnimku. Čím sme v konečnom dôsledku splnili požiadavky daného typu - len ukazujeme na podmnožinu funkcií (premenná f ukazuje na množinu funkcií, ktoré môžu vyvolať ľubovolnú výnimku, a premenná h to len špecifikuje).
Prípad 2 je z očividných dôvodov chybný, keďže by sme do premennej h priradili premennú s väčšou voľnosťou.
Pri konštruktoroch a deštruktoroch je mechanizmus zoznamov trošku komplikovanejší. Ilustrujeme to na príklade.
class A { public: A() throw (int); A(const A&) throw (float); ~A() throw(); }; class B { public: B() throw (char); B(const A&); ~B() throw(); }; class C : public B, public A { };
U triedy C to potom bude vyzerať nasledovne:
C::C() throw (int, char); C::C(const C&); C::~C() throw();
V princípe sa zoznamy u tzv. špeciálnych funkcií poskladajú - zjednotia sa množiny špecifikovaných vyvolateľných výnimiek. Jednoduché, že.
V druhej polovici si povieme o štyroch funkciách.
terminate()
unexpected()
set_terminate()
set_unexpected()
V programe existuje tzv. unexpected_handler
a terminate_handler
, pričom oba ukazujú (defaultne) na funkciu terminate()
. Tieto handlery ukazujú na funkcie s nasledujúcou hlavičkou void f(void)
.
Funkcie set_unexpected()
a set_terminate()
nám umožňujú nastaviť tieto handlery tak, aby ukazovali na nami deklarované funkcie a ich návratová hodnota sú funkcie, ktoré boli nastavené posledne.
A teraz k samotným funkciám terminate()
a unexpected()
.
unexpected()
Ak funkcia vyvolá výnimku, ktorú nemá špecifikovanú v zozname výnimiek, dôjde k tomu, že sa zavolá prv funkcia unexpected()
a tá zavolá funkciu, na ktorú ukazuje unexpected_handler
.
Ak je nastavený handler na nejakú nami deklarovanú funkciu, táto môže vyvolať výnimku. Sama nevracia hodnotu, len môže vyhodiť novú, vyhodiť pôvodnú alebo nevyhodiť žiadnu výnimku.
V prípade, že vyhodí výnimku špecifikovanú v zozname výnimiek funkcie, u ktorej prv došlo k porušeniu danej špecifikácie, je zoznam catch
blokov znova prehľadaný a pokračuje sa v normálnom behu.
V prípade, že sa toto nestane, nastávajú dva prípady.
Ak je v špecifikácii výnimiek funkcie uvedená std::bad_exception
, tak táto výnimka bude vyvolaná a pokračuje sa v hľadaní catch
bloku. Táto výnimka je vyvolaná automaticky, ak funkcia nastavená v unexpected_handler
nevyvolá žiadnu výnimku.
V prípade, že sa ani toto nestane, je zavolaná funkcia terminate()
, ktorá program ukončí.
terminate()
V určitých špecifických prípadoch je zavolaná funkcia terminate()
, ktorá zavolá terminate_handler
, alebo ak ten nieje nastavený, tak funkciu abort()
, ktorá nemilosrdne ukončí program.
Funkcia odkázaná v terminate_handler
by mala ukončiť program. Nami poskytnutá varianta by mala byť metodickou pomôckou pri debugovaní, alebo ako mechanizmus pre rozumného ukončenia programu (uloženie konzistetných štruktúr na disk atď).
Ďalším špecifickým prípadom je, keď pri hľadaní handleru (catch
bloku) na výnimku bol mechanizmus výnimiek neúspešný. Ide hlavne o prípad, kedy by boli v programe zrazu dve výnimky "naraz" alebo by nebolo možné sa o vzniknutú výnimku postarať v nejakom catch
bloku.
Dôvody pre volanie terminate()
stack-unwinding
vyvolal deštruktor výnimku a o tú nebolo postarané (existovali by 2 výnimky)throw
, sám vyhodil výnimku a o tú nebolo postaranéatexit()
, vyvolá výnimku o ktorú nebude postaranéthrow
snaží bez uvedenia operandu rethrow
vyvolať výnimku a pritom sa nerieši žiadna výnimka (čo sa potom má znova vyvolať)unexpected_handler
vyvolá výnimku, ktorá tiež nieje uvedená v zozname špecifikovaných výnimiek a v tomto zozname nieje výnimka std::bad_exception
unexpected_handler
terminate()
nemôže vyvolať výnimku ani zavolať return
.
V ďalšom dieli si povieme zásady exception-safe programovania.
Copyright © 2002 - 2013 inet.sk, s. r. o. | Všetky práva vyhradené | Neprešlo jazykovou úpravou | ISSN 1336-1899
Využívame kvalitný webhosting za rozumnú cenu od Inet.sk