Výnimky - Exceptions I

Tomáš Plch  /  08. 06. 2009, 10:23

Výnimky (Exceptions) - úvod do problematiky a jednoduché príklady použitia s vysvetlením logiky fungovania.

Na úvod tohoto článku sa zoznámime s logikou fungovania výnimiek (exceptions) a ich teoretickým použitím. Z prvej časti by si mal čitateľ najmä odniesť pochopenie, čo to výnimky sú, ako fungujú a ako ich použiť. V nasledujúcich dieloch sa čitateľ dozvie ako sa výnimky efektívne dajú použiť a čo je to "exception safe programming". 

Každý, kto naprogramoval aspoň menší program sa stretol s nutnosťou aby funkcie v prípade chybových alebo špecifických stavov vrátili špecifickú návratovú hodnotu indikujúcu chybový stav.

V jazyku C je bežne zaužívaná prax vrátiť návratový kód indikujúci stav po ukončení funkcie - celé čislo. V prípade, že sa jedná o záporné číslo je to kód chyby a v prípade návratu napríklad 0 alebo iného kladného čísla, sa jedná o úspešné ukončenie funkcie (sú aj iné metodiky, ale ide skôr o ilustráciu ako o praktický návod).

Táto metodika prináša mnoho úskalí, ktoré treba okorčuľovávať všemožnými prístupmi (globálny indikátor chyby, špecifické návratové hodnoty atď). Drsnejšie techniky, ako jumpy a long jumpy(skoky) a použitie goto dokážu zamotať hlavu aj skúseným programátorom a treba ich nahradiť lepším a hlavne bezpečnejším mechanizmom.

S príchodom jazyka C++ bolo možné a potrebné uviesť do sveta mechanizmus, umožňujúci programátorom vyvolať a detegovať vznik špecifických stavov a udalostí počas vykonávania funkcíí. Týmto mechanizmom sú práve výnimky.

K vzniku výnimiek viedol aj vznik konštruktorov a iných špecifických jazykových konštrukcií, ktoré neumožňujú aby programátor vrátil v prípade nutnosti hodnotu reprezentujúcu chybový stav - konštruktory návratové hodnoty nemajú. Iným príkladom je návrat inštancií objektu, kedy by bolo potrebné upravovať návrh programu, aby bolo možné detegovať "chybný stav" vznikom špecifickej triedy.

Dúfam, že som čitateľov dostatočne motivoval na to, aby pokračovali v čítaní, a chápali, že výnimky(exceptions) niesú samoúčelný strašiak.

Vrhnime sa teda na logiku výnimiek. Ako už určite čitateľ vie, zdrojový kód pozostáva z funkcií, ktoré pozostávajú z blokov kódu. Bloky kódu môžu mať lokálne premenné, ktoré majú svoje konštruktory a deštruktory (ak sa jedná o triedy). Lokálne premenné zanikajú pri opustení daného bloku kódu.

To len tak na zopakovanie.

Samotný mechanizmus výnimiek pozostáva z troch podstatných súčastí.

Prvou je try-catch blok, ktorý predstavuje oblasť kódu, pri ktorej vykonávaní (volanie funkcií nevynímajúc) môže vzniknúť výnimka, ktorá môže byť odchytená v bloku (blokoch) catch.

V jednoduchosti, výnimka vznikne (môže a nemusí) v bloku try a je zachytená v bloku catch ktorý je špecifikovaný aby ju zachytil. To ako sa špecifikuje zachytená výnimka si povieme v nasledujúcich odstavcoch. Výnimka môže vzniknúť v ľubovolnom kuse kódu, blok try-catch len rieši tzv. exception-handling, teda "čo ak vznikne výnimka".

Treťou podstatnou súčasťou je príkaz throw, ktorý výnimku "vyhodí". To čo sa s ňou deje ďalej, si vysvetlíme čoskoro.

Najjednoduchší príklad použitia výnimiek:

 

void f()
{
 try {
  throw 1;
  }
 catch( int ) { printf("Exception integern");}
 catch( std::string & s ) { std::cout << "Exception string:" << s << std::endl; }
 return;
}

 

Trošku zložitejšiu ilustráciu vidíme v nasledujúcom príklade:

 

void f()
{
  throw 1;
}

void q()
{
  throw std::string("E");
}

void g()
{
  try {
    f();
    q();
  }
  catch( int ) { printf("Exception integern");}
  catch( std::string & s ) { std::cout << "Exception string:" << s << std::endl; }
  return;
}

Oba príklady ilustrujú principiálne použitie výnimiek.Vysvetlíme si, čo sa deje pri vzniku a realizovaní výnimiek.

V bloku try sa vykonáva kód. Tento kód môže volať ďalšie funkcie, v princípe robiť čokoľvek. Kód, ktorý je v bloku try, je pod "dohľadom" a v prípade, že nastane vznik výnimky, je táto vypropagovaná mimo blok try a realizuje sa hľadanie výnimky v blokoch catch, podľa špecifikovaného typu. V prípade, že nebol nájdený žiadny blok catch zodpovedný za riešenie vzniknutej výnimky, pokračuje sa, akoby vznikla bez existencie bloku try - je propagovaná o úroveň vyšsie.

Pre pochopenie - výnimky môžu vznikať a nikto ich nemusí zachytiť - ciže pri volaní funkcií ktoré výnimku môžu vyvolať, nebolo použité try-catch. Takáto výnimka sa propaguje dovtedy pokiaľ na nejaký blok try-catch nenarazí. Ak nenarazí, tak je program ukončený s chybou - objaví sa hláška o vzniku výnimky - či už dialógové okno alebo hláška v konzole: terminate called after throwing an instance of 'int' Aborted. Teraz na chvíľku odbočíme ku klauzule throw a potom si presne vysvetlíme ako vzniká a propaguje sa výnimka.

Klauzula throw ma niekoľko rôznych použití. Prvým je vyhadzovanie výnimiek, druhé je tzv. rethrow, kedy zachytenú výnimku akoby vyhodíte znova a posledným použitím je špecifikovanie rozsahu výnimiek, ktoré funkcia môže vyhodiť alebo sa z nej smú propagovať.

Budeme sa zaoberať prvým použitím - vyhodením výnimky.

To sa realizuje jednoducho:


class my_exception_class { } my_exception;
throw(1); //a
throw(12.2); //b
throw('a');//c
throw(std::string("exception"));//d
throw(my_exception_class());//e
throw(my_exception);//f
throw(std::bad_alloc());//g

Jednotlivé príklady ilustrujú, že sa dá ako výnimka vyhodiť čokoľvek. Od základného typu, cez triedu až po inštanciu, prípadne štandardnú výnimku (v tomto prípade reprezentujúcu zlú alokáciu).

Prípad a,b,c je vyhodenie výnimiek základných typov. Aké to typy sú by si mal byť každý programátor schopný domyslieť.

V prípade d sa jedná o vyhodenie výnimky v podobe triedy string.

V prípade e,f je vyhodená výnimka typu my_exception_class, pričom v prípade e je nová výnimka vytvorená a predávaná ďalej, kde v prípade f sa jedná o inštanciu objektu my_exception. Varianta f je častejšie používaná z dôvodu menšieho počtu volania konštruktorov. To však záleží aj od použitia klauzúl catch.

V poslednom prípade g sa jedná o vyhodenie výnimky zo sady štandardne dostupných výnimiek v #include <exception>.

Všimnite si ale podstatné () v prípadoch e,g. Príkaz throw totiž pri vyhodení objektu musí tento objekt vytvoriť. Čo v konečnom dôsledku umožní špecifikovať aký konštruktor sa má zavolať. Na záver si vysvetlíme, ako to je s propagáciou výnimiek v spojení s blokmi kódu.

V momente keď je výnimka vyhodená, je propagovaná mimo daný blok. Pri opúšťaní každého bloku sa realizuje tzv. stack-unwinding, čo znamená, že sa zavolajú deštruktory lokálnych tried a lokálne premenné prestanú existovať (sú neplatné znížením(v praxi zvýšením) ukazateľa na vrchol zásobníku).

Takto sa opúšťa blok za blokom, až sa narazí na patričný try-catch. V klauzulách catch je vyhľadávaná klauzula ktorá by pasovala vyhodenej výnimke. To znamená, že catch( int ), catch( char ) a catch( std::string ) zachytávajú rôzne výnimky. V prípade, že nebola nájdená žiadna klauzula catch ktorá by dostatočne pasovala na vyhodenú výnimku, proces propagácie pokračuje.

Ešte drobnosť na záver. Pokračovanie v propagácii výnimky sa dá vynútiť volaním príkazu throw bez udania výnimky v rámci bloku catch. Toto zabezpečí, že sa pokračuje s prijatou výnimkou v propagácii o blok vyššie.

V nasledujúcom článku si bližie objasníme fungovanie klauzuly catch a predstavíme si tretí druh použitia klauzly throw.

Neprehliadnite: