Výnimky v C++ - Exceptions III

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 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 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()

  1. počas čistenia zásobníku - stack-unwinding vyvolal deštruktor výnimku a o tú nebolo postarané (existovali by 2 výnimky)
  2. Výraz, ktorý bol vyhodnotený v klauzule throw, sám vyhodil výnimku a o tú nebolo postarané
  3. Konštruktor alebo deštruktor nelokálneho statického objektu vyvolá výnimku a o túto nieje postarané.
  4. funkcia, ktorá bola zaregistrovaná pomocou atexit(), vyvolá výnimku o ktorú nebude postarané
  5. ak sa throw snaží bez uvedenia operandu rethrow vyvolať výnimku a pritom sa nerieši žiadna výnimka (čo sa potom má znova vyvolať)
  6. ak funkcia vyvolala výnimku, ktorú nemala špecifikovanú v zozname a funkcia pod 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
  7. pokiaľ je zavolaná defaultna hodnota v unexpected_handler

terminate() nemôže vyvolať výnimku ani zavolať return.

 

V ďalšom dieli si povieme zásady exception-safe programovania.

Neprehliadnite: