Image
16.6.2016 0 Comments

C++ / Šablóny a výnimky / 21. časť

Späť na úvod >> Späť na programovanie >> Späť na seriál

Seriál o tajomstvách C++ sa nám pomaly, ale nezadržateľne chýli ku koncu. V každej časti som sa snažil odovzdať vám maximálne množstvo informácií, napriek tomu sa dnes stretávame pri dvadsiatom prvom pokračovaní, takže náš seriál už má úctyhodných jeden a trištvrte roka. Podľa predbežného odhadu sa naposledy stretneme pri rozprávaní o zákutiach C++ v decembri tohto roku. Otázkou zostáva, čo ďalej. Preto by som sa vás rád spýtal, či máte záujem o ďalšie informácie z môjho pera (lepšie povedané, z mojej klávesnice), a ak áno, v akej forme (seriál, séria článkov, občasníky) a hlavne na akú tému. Prípadné námety posielajte mailom, moju adresu nájdete v tiráži časopisu. Ak mi to vedomosti, váš záujem a, samozrejme, predovšetkým záujem redakcie PC REVUE dovolia, azda sa budeme stretávať aj v budúcom roku s magickým číslom 2000.

Dnes sa budeme zaoberať veľmi sofistikovanou témou šablón v C++. Šablóny sú súčasťou jazyka už dávno a aj staršie prekladače dokážu s nimi pracovať; okrem toho je takmer celá štandardná knižnica C++ postavená na šablónach, takže porozumieť práci s nimi je nevyhnutné na využívanie všetkých možností, ktoré nám dnešné moderné prekladače C++ poskytujú. Okrem toho si povieme niečo o mechanizme generovania a spracovania výnimiek v C++ a aj o práci so štruktúrovanými výnimkami v jazyku C.

Deklarácia šablón

Šablóna v C++ predstavuje množinu príbuzných deklarácií. Pomocou šablón môžeme deklarovať množinu funkcií alebo množinu tried. Každá takáto množina je parametrizovaná jedným či viacerými parametrami. Na rozdiel od parametrov funkcií parametrami šablón môžu byť aj názvy typov.

Deklarácia šablóny vyzerá takto:

template < zoznam-argumentov > deklarácia

Zoznam-argumentov je čiarkami oddelený zoznam deklarácií argumentov šablóny. Môže ísť jednak o klasické deklarácie argumentov, totožné s deklaráciami argumentov funkcií, a jednak o deklarácie typových argumentov v tvare:

class identifikátor

alebo

typename identifikátor

Druhý spôsob je novší a niektoré staršie prekladače ho nemusia podporovať.

Deklarácia je bežnou deklaráciou funkcie alebo triedy (pozri ďalej). Deklarácia šablóny sa môže vyskytovať iba na úrovni súboru (t. j. ako globálna deklarácia). Pre novo deklarované meno platia všetky bežné pravidlá pre rozsah platnosti a spôsob prístupu.

Šablóny funkcií

Najprv sa budeme zaoberať deklaráciou šablón funkcií. Takáto deklarácia je v podstate ekvivalentná deklarácii nekonečnej (z hľadiska počtu existujúcich typov a ich variácií) množiny navzájom sa prekrývajúcich funkcií s rovnakým názvom, ale rozdielnym typom argumentov.

Typickým príkladom je funkcia max() s dvoma argumentmi rovnakého typu, ktorá vráti väčší z nich. Často potrebujeme viacero verzií takejto funkcie pre rôzne doménové typy. Samozrejme, vždy môžeme definovať niekoľko prekrytých funkcií, ako int max(int, int), double max(double, double) atď., ale všetky tieto funkcie budú mať rovnaké telo – líšiť sa budú len typom argumentov a návratovej hodnoty. V takomto prípade je oveľa elegantnejšie deklarovať jedinú funkciu max() ako šablónu s jedným typovým argumentom:

template <class T>
T max(T a, T b)
{
  return (a > b) ? a : b;
}

Táto deklarácia (a súčasne definícia) negeneruje nijaký kód, je to skutočne iba šablóna na vytvorenie príslušnej funkcie v okamihu, keď ju bude treba. Takýto okamih nastane vtedy, keď použijeme funkciu max() niekde v rámci programu. Typ argumentu šablóny sa implicitne odvodí z typu použitých argumentov funkcie max():

int c = max(2, 1);
double d = max(7.0, 8.0);

V prvom prípade sa vygeneruje a použije funkcia max<int>(), v druhom funkcia max<double>(). Tu vidíme výhodu šablónových funkcií: kód ich tela sa vygeneruje prekladačom iba vtedy, keď ho skutočne potrebujeme a použijeme.

Niekedy chceme prekladaču explicitne naznačiť, ktorú inštanciu má v danom prípade použiť. To môžeme zabezpečiť uvedením požadovaného typového argumentu do špicatých zátvoriek za názov funkcie:

int e = max<int>(3, 'z');

V tomto príklade je druhý argument funkcie max() typu char. Prekladač je schopný vygenerovať funkciu max(int, int) alebo max(char, char), ale nie max(int, char), preto by za normálnych okolností ohlásil chybu. Explicitné uvedenie max<int> spôsobí, že prekladač použije (zhodou okolností už predtým vygenerovanú) funkciu max(int, int).

Vygenerovanie príslušnej funkcie môžeme zabezpečiť aj inak, bez toho, aby sme museli túto funkciu niekde volať. V našom príklade vytvoríme inštanciu long max(long, long) jednoducho takto:

template long max(long, long);

alebo aj takto:

template long max<long>(long, long);

Takéto explicitné vytváranie inštancií má zmysel napríklad vtedy, ak vytvárame .LIB súbory, v ktorých chceme mať príslušnú inštanciu „natvrdo“, aby si ju nikto nemohol neskôr predefinovať.

Pýtate sa, či je možné predefinovať existujúcu funkciu? Odpoveď znie: áno, pokiaľ ide o takú inštanciu šablóny, ktorá dosiaľ nebola použitá a teda ani vygenerovaná. Ak totiž niekde v programe klasickým spôsobom deklarujeme a definujeme funkciu, ktorej prototyp sa bude zhodovať s prototypom inštancie niektorej šablónovej funkcie, bude túto novú funkciu prekladač považovať za právoplatnú inštanciu danej šablóny. V našom príklade môžeme teda pred použitím funkcie max() s argumentmi typu double uviesť takúto deklaráciu:

double max(double, double);

a prekladač namiesto vygenerovania novej inštancie double max<double>(double, double) podľa šablóny použije nezávisle definovanú funkciu double max(double, double).

Zavedenie šablónových funkcií mierne modifikuje pravidlá hľadania tej verzie prekrytých funkcií, ktorá sa použije pri danom volaní (pozri predchádzajúcu časť seriálu). V prvom rade prekladač hľadá funkciu, ktorej počet a typy argumentov sa presne zhodujú s počtom a typmi argumentov použitých pri volaní funkcie. Ak takú nenájde, pokúsi sa nájsť takú deklaráciu šablóny, z ktorej je možné vygenerovať funkciu s presnou zhodou argumentov. Ak ani teraz neuspeje, pokračuje v hľadaní bežným spôsobom. Ak v druhom kroku nájde viac ako jednu vhodnú šablónu, ohlási chybu nejednoznačnosti. Pri hľadaní vhodnej šablóny sa neuvažujú nijaké konverzie (ani triviálne), ako sme videli pri volaní max(3, 'z').

Pre deklaráciu šablónových funkcií platí určité obmedzenie: pokiaľ nie je explicitne určená konkrétna inštancia šablóny, musia všetky typové argumenty šablóny vystupovať v deklaráciách formálnych argumentov šablónovej funkcie. Prekladač totiž musí na základe volania funkcie vedieť, ktorú inštanciu má vygenerovať. To sa mu nepodarí napríklad v prípade:

template <class T>
T foo() { /* ... */ }
 
int a = foo();

I keď vidíme, že by sa mala vygenerovať inštancia foo<int>(), prekladač si to, bohužiaľ, nie je schopný domyslieť a ohlási chybu. Jediná možnosť, ako ho informovať o tom, čo chceme, je takáto:

int a = foo<int>();

Podobné pravidlo platí pre netypové argumenty, ale tie sa používajú skôr pri deklaráciách šablón tried, preto sa tu nimi nebudeme hlbšie zaoberať.

Šablóny tried

Deklarácia šablón tried má podobný význam ako deklarácia šablón funkcií. Šablóna triedy predstavuje parametrizovaný vzor na tvorbu nových tried. Na rozdiel od šablón funkcií majú šablóny tried oveľa širšiu oblasť použitia, umožňujúc pomerne rýchly vývoj programov na vysokej úrovni abstrakcie.

Ako príklad si vytvoríme šablónu pre triedu vector, ktorá bude predstavovať dynamicky alokované pole prvkov nejakého typu. Tento typ prvku bude parametrom šablóny a aby sme si ukázali aj iné ako typové parametre, budeme veľkosť poľa zadávať nie ako parameter konštruktora (čo by sme spravili za normálnych okolností), ale ako parameter šablóny. Uvedomte si však, že takto definovaná šablóna by vytvárala samostatnú triedu pre každú použitú veľkosť poľa v programe, čo obyčajne nepotrebujeme a nechceme.

Deklarácia šablóny triedy vector bude vyzerať takto:

template <class T, int n>
class vector
{
  T* ptr;
public:
  vector();
  ~vector();
  T& operator[](int i)
  { return ptr[i]; }
};

Pre jednoduchosť sme do triedy vector zaviedli len jedinú členskú funkciu: prekrytý operátor indexovania pre prístup k jednotlivým prvkom vektora. Parameter šablóny T je použitý ako doménový typ ukazovateľa ptr, reprezentujúceho dynamické pole prvkov.

Takto deklarovaná šablóna, samozrejme, nie je úplná, prekladač dosiaľ nevie, ako konštruovať a deštruovať inštancie triedy vector. Jednotlivé členské funkcie, pokiaľ ich nedefinujeme v rámci deklarácie triedy, treba definovať ako šablónové, s rovnakými parametrami šablóny ako ich materská trieda:

template <class T, int n>
vector<T, n>::vector()
{
  ptr = new T[n];
}
template <class T, int n>
vector<T, n>::~vector()
{
  delete [] p;
}

Všimnite si, že členské funkcie kvalifikujeme menom ich triedy, ktoré však v tomto prípade nie je vector, ale vector<T, n>, pretože ide o šablónu triedy. Aj tu však existuje výnimka – v názvoch konštruktora a deštruktora už parametre šablóny nesmieme uvádzať.

Triedu vector<T, n> môžeme odteraz veselo používať, len musíme uviesť oba parametre šablóny, t. j. napríklad takto:

vector<int, 20> v;
v[10] = 2178;

Premenná v bude predstavovať inštanciu triedy vector<int, 20>, čo je inštancia šablóny vector<T, n>. Prekladač automaticky po zhliadnutí deklarácie v vygeneruje telo členských funkcií a prípadné statické premenné triedy vector<int, 20>. Meno novej triedy je plnohodnotným menom triedy a môžeme ho ďalej používať ako každé iné meno typu. Dokonca môžeme toto meno použiť ako parameter ľubovoľnej šablóny – aj tej istej:

vector<vector<int, 20>, 5> v2;

Premenná v2 bude predstavovať vektor piatich prvkov, z ktorých každý bude sám osebe vektorom dvadsiatich celých čísel.

Vzhľadom na skutočnosť, že šablónová trieda je trieda ako každá iná, môžeme od nej bez problémov odvodzovať nové triedy:

class newvector : public vector<char, 100>
{ /* ... */ };

Častejšie však bude odvodená trieda sama deklarovaná pomocou šablóny.

Pre explicitné vygenerovanie inštancie nejakej šablóny triedy platí to isté, čo sme uviedli v odseku o šablónach funkcií. Ak teda chceme prekladaču prikázať, aby vygeneroval inštanciu šablóny vector<double, 200>, použijeme zápis:

template class vector<double, 200>;

Akákoľvek deklarácia triedy, ktorej meno sa zhoduje s menom konkrétnej inštancie niektorej šablóny, je považovaná za inštanciu tejto šablóny. V praxi teda môžeme deklarovať napríklad triedu vector<float, 99>, ktorá bude mať úplne iné údajové členy a členské funkcie ako generická trieda vector<T, n>. Napriek tomu bude trieda vector<float, 99> považovaná za inštanciu šablóny vector<T, n>.

Keďže sa všetky členské funkcie šablónovej triedy automaticky považujú za šablónové funkcie, nič nám nebráni definovať si ich podľa potreby inak. Musíme to však spraviť skôr, než prekladač vygeneruje ich telo automaticky na základe vytvorenia inštancie príslušnej triedy. Toto predefinovanie bude vyzerať takto:

vector<long, 3>::vector()
{ /* ... */ }

a spôsobí, že pri vytváraní objektov triedy vector<long, 3> sa nepoužije generický konštruktor zo šablóny, ale táto jeho špeciálna verzia.

Ak v rámci deklarácie šablóny triedy uvedieme nejakú funkciu ako spriatelenú (friend), nestáva sa táto funkcia automaticky šablónovou. Ukážme si príklad:

template <class T>
class foo()
{
  // ...
  friend void fnc1();
  friend void fnc2(foo<T>*);
  friend void fnc3(foo*);
};

Funkcia fnc1() bude spriatelenou funkciou všetkým triedam foo<T>, ktoré prekladač vygeneruje. Funkcia fnc2() musí byť deklarovaná ako šablóna, pretože jej parametrom je ukazovateľ na šablónovú triedu foo<T>. Jej deklarácia môže vyzerať napríklad takto:

template <class T>
void fnc2(foo<T>*)
{ /* ... */ }

Každá vygenerovaná inštancia šablóny foo<T> bude mať vlastnú spriatelenú funkciu fnc2(foo<T>*). Konečne deklarácia funkcie fnc3() je chybou, pretože neexistuje samostatná trieda foo, iba jej konkrétne varianty, ako foo<int>, foo<double> a pod.

Statické premenné a členy tried

V tele šablónovej funkcie môžeme deklarovať statické premenné. Každá inštancia šablónovej funkcie bude mať vlastnú množinu statických premenných, rovnako bude mať vygenerovaný vlastný kód, predstavujúci telo funkcie. Statické premenné, samozrejme, môžu byť parametrizované pomocou parametrov šablóny. Malý príklad:

template <class T>
void swap(T& a, T& b)
{
  static T tmp;
  tmp = a;
  a = b;
  b = tmp;
}

Ak funkciu swap() použijeme napríklad takto:

int x, y;
double m, n;
swap(x, y);
swap(m, n);

bude v inštancii swap(int&, int&) existovať statická premenná tmp typu int a v inštancii swap(double&, double&) statická premenná tmp typu double. Obe premenné tmp budú od seba nezávislé.

Podobne to platí pre statické členy tried, len tu musíme dať pozor: statické údajové členy je potrebné definovať samostatne, mimo deklarácie triedy. Preto pre každú vygenerovanú inštanciu danej šablóny (automaticky či explicitne) musíme zabezpečiť definíciu všetkých jej statických členov, ovplyvňovaných parametrami šablóny, na globálnej úrovni:

template <class T>
class foo()
{
  // ...
  static T s;
};
 
int foo<int>::s = 123;
char* foo<char*>::s = "hello";

Ak deklarácia statických členov nie je ovplyvňovaná parametrami šablóny, postačí samozrejme každý statický údajový člen definovať raz, ale musíme použiť podobnú syntax, ako pri deklarácii samotnej šablóny:

template <class T>
class bar()
{
  // ...
  static int s;
};
 
template <class T>
int bar<T>::s = 216;

Takáto deklarácia zabezpečí, že každé vygenerovanie inštancie šablóny triedy bar<T> spôsobí automatickú definíciu a inicializáciu jej statického člena s. Pokiaľ chceme pre konkrétnu inštanciu inicializovať jej člen s inou hodnotou ako implicitných 216, použijeme zápis:

int bar<double*>::s = 78;

Výnimky v C++

Ďalšou pokročilou črtou jazyka C++ je existencia výnimiek. Mechanizmus výnimiek v podstate slúži na dokonalejšie a prirodzenejšie ošetrovanie výnimočných stavov, ku ktorým môže dôjsť počas behu programu (typicky nedostatok voľnej pamäte, neexistencia súboru na disku a pod.). Klasický spôsob detekcie chýb vyzerá asi takto: majme funkciu, ktorá má na starosti nejakú činnosť. Táto funkcia nech oznamuje výskyt prípadnej chyby špeciálnou návratovou hodnotou. Po každom zavolaní takejto funkcie musíme zisťovať, čo nám vrátila a či náhodou nedošlo k chybe. Teda ak funkciu voláme za sebou päťkrát, musíme do kódu doplniť päť príkazov if. Zdrojový text programu sa stáva neprehľadným, nehovoriac o tom, že ľahko môžeme niektorý z testov omylom vynechať. Program potom nie je dostatočne robustný, pri výskyte chyby môže jednoducho padnúť a chudák používateľ bude mať na čo nadávať. Okrem toho nie každá funkcia si môže dovoliť vyhradiť jednu špeciálnu návratovú hodnotu na indikáciu chyby.

Výnimky boli vymyslené práve preto, aby nám pomohli vyriešiť problémy naznačené v predchádzajúcom odseku. Ich princíp je jednoduchý: chybový stav sa bude indikovať vygenerovaním (aj „vyhodením“, z anglického throw) výnimky. Blok príkazov, v ktorom môže dôjsť k výnimke, budeme strážiť a v prípade, že k nej naozaj dôjde, budeme na túto skutočnosť patrične reagovať pomocou tzv. handlera výnimky (odmietam toto slovo preložiť do slovenčiny, pretože „ovládač“ to nie je a „obslužný kód“ je trochu krkolomné).

V C++ celá situácia bude vyzerať takto. Strážený blok príkazov (tiež nazývaný try-blok) má tvar:

try zložený-príkaz zoznam-handlerov

Zoznam-handlerov je bielymi znakmi oddelená postupnosť handlerov v tvare:

catch ( deklarácia-výnimky ) zložený-príkaz

V rámci stráženého bloku môžeme výnimku vygenerovať pomocou výrazu:

throw výraz

Tento výraz je typu void.

Ukážme si na jednoduchom príklade, ako celý mechanizmus vyzerá:

void main()
{
  try
  {
    // ...
    throw "chyba!";
    // ...
  }
  catch (const char* str)
  {
    printf("výnimka: %s\n", str);
  }
}

Len čo program dospeje k príkazu throw, skočí na koniec bloku try a hľadá vhodný handler. Pri hľadaní jednoducho porovnáva typ vyhodeného objektu (v našom prípade const char*) s deklaráciou uvedenou v hlavičke handlera. Ak nájde vhodný handler, vykoná jeho telo a ďalej pokračuje kódom, ktorý nasleduje za všetkými handlermi príslušného try bloku. No ak taký handler nenájde, ukončí funkciu, v ktorej sa nachádza, a prejde do nadradenej (volajúcej) funkcie, v ktorej opätovne hľadá vhodný handler rovnakým spôsobom. Ak v nadradenej funkcii nie je volanie funkcie, ktorá výnimku vyhodila, uzavreté do try bloku, rovno túto funkciu opustí. Takto prechádza celý zásobník volaných funkcií, až kým nenájde vhodný handler alebo kým nedôjde k pokusu o opustenie funkcie main(). Vtedy zavolá funkciu terminate(), ktorá implicitne ukončí program.

Objekt, ktorý je vyhodený pomocou príkazu throw, môže byť ľubovoľného typu, najčastejšie asi to bude inštancia nejakej vhodnej triedy. Inicializácia formálneho objektu v deklarácii hlavičky handlera je prakticky zhodná s inicializáciou formálneho argumentu funkcie. Niekedy nepotrebujeme do handlera odovzdať konkrétny objekt, ale iba informáciu o type výnimky. Preto je možné v hlavičke handlera vynechať identifikátor vyhodeného objektu – v takom prípade však k nemu nemáme nijaký prístup:

class dummy {};
 
try
{
  // ...
  throw dummy();
  // ...
}
catch (dummy)
{
  // ...
}

Ako sme už povedali, pri hľadaní vhodného handlera sa porovnáva typ vyhodeného objektu s typom deklarovaným v hlavičke handlera. Platí tu nasledujúce pravidlo: handler s deklarovaným typom T, const T, T&, const T& zachytí objekt typu E, ak

  • T a E sú rovnaké typy,
  • ak T je prístupnou základnou triedou E,
  • ak T aj E sú ukazovatele a E možno pretypovať na T pomocou štandardných konverzií.

Handlery sú testované v poradí ich deklarácie, preto je chybou uviesť handler zachytávajúci objekt odvodenej triedy za handlerom zachytávajúcim objekt základnej triedy – druhý z handlerov sa k slovu už nedostane.

Zvláštnym prípadom handlera je tzv. univerzálny handler, deklarovaný podobne ako pri funkciách s premenným počtom argumentov pomocou výpustky (...). Takýto handler zachytí akúkoľvek výnimku. Je dobré ho dávať až na koniec celej hierarchie, aby sme si omylom neodchytili nesprávnu výnimku. Ak nechceme, aby nám nejaká výnimka ukončila program predčasne, môžeme univerzálny handler dať na koniec funkcie main():

void main()
{
  try {
    // ...
  }
  catch (...)
  {
    // ...
  }
}

Niekedy treba odovzdať výnimku ďalej, vyššiemu handleru. Aj na takúto situáciu C++ pamätá – pokiaľ uvedieme kľúčové slovo throw bez argumentov, aktuálne vyhodený objekt sa pošle vyššie v celej hierarchii. Toto však môžeme spraviť iba v rámci nejakého handlera, inokedy by totiž nebolo čo „poslať ďalej“.

Najdôležitejšou črtou celého mechanizmu výnimiek je však skutočnosť, že v okamihu, keď sa nájde príslušný handler, program automaticky deštruuje všetky automatické objekty, ktoré boli skonštruované od vstupu do try-bloku, patriaceho k nájdenému handleru až po okamih výskytu výnimky a to aj cez niekoľko úrovní funkcií! Tento proces sa nazýva aj „odrolovanie“ zásobníka (v angličtine stack unwinding). Ukážme si príklad:

class C
{
public:
  C();
  ~C();
};
 
C::C()
{
  printf("constructing C\n");
}
 
C::~C()
{
  printf("destructing C\n");
}
 
void fnc2()
{
  throw 1;
}
 
void fnc1()
{
  C c2;
  fnc2();
}
 
void main()
{
  try
  {
    C c1;
    fnc1();
  }
  catch (...)
  {
    printf("caught exception!");
  }
}

Ak si vyskúšate tento príklad preložiť a spustiť (budete však potrebovať niektorý z novších prekladačov podporujúcich výnimky – starý dobrý Borland C++ 3.1 vám nebude stačiť), uvidíte, že skôr, než sa vykoná telo univerzálneho handlera, zavolajú sa deštruktory objektov c1 aj c2.

Deklarácia povolených výnimiek

Deklaráciu každej funkcie môžeme doplniť zoznamom výnimiek, ktoré táto funkcia môže generovať. Tento zoznam sa skladá z kľúčového slova throw a v zátvorkách uzavretej množiny typov objektov, ktoré môžu byť z funkcie „vyhodené“. Príklad:

class C { /* ... */ };
 
void fnc() throw (int, C, char*)
{
  // ..
}

Funkcia, ktorá takúto špecifikáciu neobsahuje, môže vyhodiť ľubovoľnú výnimku. Funkcia, ktorá obsahuje prázdnu špecifikáciu, throw(), nesmie vyhodiť nijakú výnimku. Funkcia, ktorá má povolené vyhodiť výnimku typu triedy C, môže vyhodiť aj výnimku typu triedy verejne odvodenej od C. Ak počas behu programu nastane situácia, že by funkcia mala vyhodiť výnimku, ktorú nemá povolenú, zavolá sa funkcia unexpected().

Špeciálne funkcie

V súvislosti s výnimkami v C++ sme spomenuli dve funkcie so zvláštnym postavením – terminate() a unexpected(). Prvú z nich prekladač zavolá za týchto okolností:

  • keď nenájde vhodný handler pre vygenerovanú výnimku,
  • keď počas hľadania handlera zistí porušenie zásobníka,
  • keď počas odrolovania zásobníka niektorý z volaných deštruktorov sám vyhodí výnimku

Implicitne funkcia terminate() volá funkciu abort(), čo má za následok okamžité a násilné ukončenie programu. Pomocou funkcie set_terminate() s argumentom typu void (*)() môžeme nastaviť vlastnú obsluhu, ktorá však musí program ukončiť; pokus o návrat z terminate() je chybou. Funkcia set_terminate() vracia ako návratovú hodnotu ukazovateľ na predchádzajúcu obslužnú funkciu; umožňuje tak zreťaziť niekoľko vlastných funkcií.

Druhú z funkcií, unexpected(), zavolá prekladač vtedy, keď zistí, že sa niektorá funkcia snaží vyhodiť výnimku, ktorú explicitne nedeklarovala v svojom zozname povolených výnimiek. Implicitne funkcia unexpected() volá funkciu terminate(). Vlastné správanie môžeme definovať pomocou funkcie set_unexpected(), ktorá má opäť jediný argument typu void (*)() a takisto vracia ukazovateľ na predchádzajúcu obslužnú funkciu.

Štruktúrované výnimky

Na záver tejto časti si povieme pár slov o téme, ktorá nie je súčasťou normy C++. Štruktúrované výnimky sú určené pre jazyk C, ale ani tam nie sú nijako normatívne definované. Ich podpora jednotlivými prekladačmi závisí čisto od „dobrej vôle“ ich výrobcov, ale prácu s nimi umožňujú všetky moderné prekladače, hoci stopercentne to môžem tvrdiť iba o Microsoft Visual C++. Nasledujúce informácie sú preto založené na dokumentácii k tomuto produktu a berte ich len ako stručnú informáciu o tom, ako je možné pracovať s výnimkami aj v jazyku C.

Na prácu so štruktúrovanými výnimkami máme k dispozícii niekoľko kľúčových slov: __try, __except, __finally, __leave. Všimnite si, že všetky sa začínajú dvoma znakmi podčiarknutia, čo vyjadruje ich neštandardnosť a závislosť od implementácie. Základná schéma stráženého bloku je podobná ako v C++:

__try zložený-príkaz1
__except ( výraz ) zložený-príkaz2

Prvý zložený príkaz predstavuje strážený blok. Ak sa počas vykonávania príkazov v tomto bloku nevyskytne výnimka, program pokračuje za zloženým príkazom v klauzule __except. Výskyt výnimky v bloku spôsobí vyhodnotenie výrazu, na základe ktorého sa rozhodne, čo ďalej. Možnosti sú tri (ide o preddefinované konštanty):

- EXCEPTION_CONTINUE_SEARCH – výnimka nie je rozpoznaná, hľadanie vhodného handlera pokračuje na vyššej úrovni (ekvivalentné príkazu throw bez argumentov);

- EXCEPTION_CONTINUE_EXECUTION – výnimka je rozpoznaná, ale ignoruje sa, program pokračuje ďalej od toho bodu, v ktorom k výnimke došlo;

- EXCEPTION_EXECUTE_HANDLER – výnimka je rozpoznaná, vykoná sa príslušný handler (druhý zložený príkaz) a na rozdiel od C++ vykonávanie programu pokračuje od bodu, v ktorom výnimka nastala.

Uprostred bloku __try/__except je možné použiť kľúčové slovo __leave, ktoré spôsobí okamžité opustenie bloku a presmerovanie programu na prvý príkaz za klauzulou __except.

Druhý variant stráženého bloku používa kľúčové slovo __finally a má tvar:

__try zložený-príkaz1
__finally zložený-príkaz2

Na rozdiel od predchádzajúceho prípadu tu nie je nijaký handler pre výnimky. Zmysel sekcie uvedenej slovom __finally je jej zaručené vykonanie nezávisle od toho, či bude strážený blok ukončený riadnym spôsobom až do konca alebo bude opustený skôr v dôsledku vzniku výnimky. Do tejto ukončovacej sekcie sa preto obyčajne dáva kód slúžiaci na uvoľnenie alokovaných prostriedkov a iné upratovacie činnosti. Aj uprostred bloku __try/__finally je možné použiť kľúčové slovo __leave.

Na záver zostáva povedať, že hoci je možné miešať výnimky jazyka C++ a štruktúrované výnimky jazyka C, nie je to práve najrozumnejší nápad. Štruktúrované výnimky sú vždy typu unsigned int a sú zachytiteľné bežným C++ handlerom (catch), ale vzhľadom na nepomerne širšie možnosti C++ výnimiek nemá veľmi zmysel ich používať (jedine, keby ste mali niečo do činenia s existujúcim kódom v jazyku C).

Efektivita

Na začiatku rozprávania o výnimkách sme sa zmienili o tom, čo viedlo k zavedeniu mechanizmu výnimiek do C++ a aké výhody nám plynú z ich používania. Bohužiaľ, každá minca má dve strany a programovanie za pomoci výnimiek nás niečo stojí. To niečo je výkon programu, ktorý pri nadmernom používaní výnimiek dosť radikálne klesá – uvedomte si rozdiel medzi bežným návratom z funkcie (niekoľko inštrukcií) a návratom na základe vyhodenia výnimky (zložitý kód – nájdenie handlera, odrolovanie zásobníka). Ak vás preto napadlo vracať výsledok volania nejakej funkcie výnimkou, tak na to rýchlo zabudnite, cena je privysoká. Výnimky majú zmysel pri rozsiahlych programoch, kde by klasické ošetrovanie chybových stavov neúmerne zneprehľadnilo program. Určité spomalenie takýchto programov je nevyhnutnou daňou za nezbláznenie sa pri ich tvorbe.

Na záver

V budúcej časti sa budeme zaoberať povestnými čerešničkami na torte – inak povedané tými najnovšími (i o niečo staršími) črtami, ktoré v C++ existujú. Verte mi, že táto časť bude v mnohom poučná aj pre mňa, sám totiž mnohé veci aktívne neovládam, i keď som o nich už počul.

C++

Nechajte si posielať prehľad najdôležitejších správ emailom

Mohlo by Vás zaujímať

Ako na to

Ako zbaviť fotky hmly

08.12.2016 11:59

Hmla alebo dym sú často veľmi kreatívne nástroje. No všetkého veľa škodí. Fotka potom stráca kontrast a v podstate na nej nič nevidieť. Hmlu môžete neraz následnými úpravami odstrániť alebo zredukovať ...

Ako na to

Užitočné SW nástroje

08.12.2016 11:53

AllDup v4.0.3 Určenie: program na vyhľadávanie a odstraňovanie duplicitných súborov Vlastnosti: duplicitné súbory sa vyhľadávajú len na zvolených diskových jednotkách alebo len v rámci vybraných ...

Ako na to

Fotografovanie s bleskom

08.12.2016 11:47

Ak máte moderný fotoaparát so vstavaným alebo externým bleskom, zdá sa vám téma článku triviálna. Jednoducho nastavíte vhodný režim, vyberiete najlepšiu kompozíciu záberu, exponujete a o zvyšok sa už ...

Žiadne komentáre

Vyhľadávanie

Kyocera - prve-zariadenia-formatu-a4-s-vykonom-a3

Najnovšie videá