Vyjímky. Pod pojmem výjimka se rozumí nějaká výjimečná situace, která nastane v dané funkci. V jazyce C i C++ se často používá návratových hodnot funkcí, které vracejí úspěšnost provádění nějaké operace (např. -1, -2, -3, ... se vrací v případě chyby, jinak se vrací 0 jako bez chyby). Chceme-li v posloupnosti několika řádků zdrojovém textu takto ošetřit všechny možné chyby, vznikne nám velice nepřehledný program plný vnořených příkazů if. Jinou možnost nám nabízí mechanizmus výjimek. Princip spočívá v tom, že označíme posloupnost příkazů do zvláštního bloku (try), říká se mu často hlídaný blok, ve kterém neošetřujeme žádné chyby. Právě v tomto bloku může vzniknout výjimka. Za tímto blokem označíme postupně jaké výjimky mohly v hlídaném bloku nastat, a jak je ošetřit. V C++ může být výjimkou proměnná jakéhokoliv primitivního datového typu, nebo instance jakékoliv třídy. Výjimka by v sobě měla nést nějakou informaci o situaci, která nastala a proč byla vyvolána.
Syntaxe vyjímek. Výjimka se vyvolá (vyvrhne) pomocí klíčového slova throw. Hlídaný blok se značí klíčovým slovem try. K odchycení vyvolaných výjimek slouží klíčové slovo catch. Vše uvedu na jednoduchém příkladu: Definice a implementace třídy, která reprezentuje (zpracovává) vyjímku. #include
#include <string> using namespace std; // Trida vyjimek class Vyjimka { private: string Duvod;
// textovy popis
public: // Přístupove funkce void nastav(string d) { Duvod = d; } string dej() { return Duvod; } }; ostream &operator<< (ostream &os, Vyjimka &v) { return os << v.dej() << endl; } Výše definovaná výjimka jako třída poskytuje pouze textovou informaci. Pro textovou informaci jsou definované přístupové funkce. Mimo jiné je definovaná i nečlenské metoda operator <<, pro použití streamu. Třída používá STL knihovnu string. Knihovna zajišťuje zpracování řetězce, STL knihovny budou vysvětleny v tomto semestru později!
Definice třídy Zlomek. Třída provádí dělení čitatele jmenovatelem (operaci zlomku). Třída obsahuje přístupové metody nastavení hodnot jmenovatele a čitatele. Obsahuje metodu vydel(...). class Zlomek { private: // Citatel, jmenovatel int C,J; public: // Pristupove funkce void nastavCitatel(int c) { C = c;} void nastavJmenovatel(int j) {J = j;} // Deleni osetreno vyjimkou double vydel() throw (Vyjimka); }; double Zlomek::vydel() throw (Vyjimka) { // zacatek bloku 2 int *i = new int; if (J == 0) { //zacatek bloku 1 string s("Nejde"); Vyjimka v; v.nastav(s); throw v; } // konec bloku 1 delete i; return ((double)C / J); } // konec bloku 2 Právě metoda vydel(...) má definovanou vyjímku. V deklaraci metody vydel(...) se zde objevilo další klíčové slovo throw. Za slovem throw následuje seznam typů výjimek, které mohou být z dané metody (nebo i funkce) vyvrženy. V případě naší metody vyděl(...) se jedná o výjimky třídy Vyjimka. Nedoporučuji používat jako výjimky primitivní datové typy, i když syntaxe jazyka do umožňuje. V případě, že atribut J třídy zlomek bude v momentě zavolání metody vydel(...) roven nule, dojde k vyvolání výjimky. V tomto případě nejprve vytvořím instanci třídy Vyjimka, a poté ji příkazem throw vyvrhnu. Při vyvržení výjimky dojde k okamžitému opuštění aktuálního bloku daného závorkami { }. V našem příkladě jsem jej poznámkami označil jako blok 1. Výjimka byla z tohoto bloku vyvržena. Při opuštění tohoto bloku dojde k zavolání destruktorů všech lokálních instancí tak, jako by se jednalo o korektní opuštění bloku. V našem případě s a v. Nemusíte se obávat, že byla zlikvidována instance v, kterou bude v
budoucnu používat. Budete totiž pracovat s její kopií. Po opuštění bloku se výjimka šíří dále. Dojde k vyvržení výjimky z "vnějšího" bloku, v naše případě označeného jako blok 2. V tomto bloku budou opět korektně zlikvidovány všechny lokální instance. V našem případě bude zlikvidován ukazatel, ale nedojde k uvolnění paměti, na kterou ukazuje. Později ukážu, jak tento problém řešit. Příklad vstupní funkce aplikace main(...): int main(void) { // instance tridy vyjimka a inicializace citatele Zlomek z1,z2; z1.nastavCitatel(10); z2.nastavCitatel(5); for(int i = 5; i > -5; i--) { // nastaveni jmenovatele z1.nastavJmenovatel(i); z2.nastavJmenovatel(i); // proces deleni s moznosti odchytavani vyjimek try { // zkouseno deleni cout << "10 / " << i << " = " << z1.vydel() << endl; cout << "5 / " << i << " = " << z2.vydel() << endl; } catch (Vyjimka v) { // odchyceni vyjimky (trida Vyjimka) cout << v << endl; // pouziti vyse definovane metody // pro stream operace } } return 1; } Ve funkci main(...) je prováděno dělení s možným odchytem vyjímky v bloku označeným klíčovým slovem try. Při vyvržení výjimky z hlídaného bloku se zjistí, jestli za hlídaným blokem existuje pro tento typ výjimky odchycení catch. Jestliže ne, výjimka je vyvržená dále z aktuálního bloku. Jestliže ano, dojde k vykonání tohoto bloku označeného catch. Po odchycení a ošetření výjimky program normálně pokračuje příkazy za blokem catch. Jak jsem se již zmínil, při
vyvolání výjimky dojde k likvidaci všech lokálních proměnných. Je-li ale lokální proměnnou ukazatel, nebo reference, nedojde k uvolnění dat, na které "ukazují". Do našeho příkladu jsem úmyslně vložil do metody vydel(...) ukazatel na int. Při vyvolání výjimky se řádek delete neprovede. Paměť na kterou ukazatel ukazuje zůstane neuvolněná. Bylo by vhodné vyvrženou výjimku ošetřit na více místech. (V mém jednoduchém příkladě bych mohl jednoduše před slovo throw napsat delete i, já ale chci ukázat jak ošetřit výjimku na více místech). Opravme metodu vydel(...) následovně: double Zlomek::vydel() throw (Vyjimka) { int *i = new int; try { if (J == 0) { //zacatek bloku 1 string s("Nejde"); Vyjimka v; v.nastav(s); throw v; } // konec bloku 1 delete i; } catch (Vyjimka v) { // Ošetrim, co mùžu delete i; throw; // V tomto pripade má stejný význam jako throw v; } return ((double)C / J); } Výjimku jsem odchytil ještě v metodě vydel(...). Uvolnil jsem paměť, na kterou ukazuje ukazatel i, a poté jsem výjimku opět vyvrhl. Jsme-li v bloku catch a chceme-li odchycenou výjimku poslat dále, nemusíme uvádět její název. Podívejme se ještě podrobněji na deklarace metod, ze kterých může být vyvržena výjimka. Metoda vydel(...) je deklarována: double vydel() throw (Vyjimka);. Znamená to, že z metody vydel(...) může být vyvržena výjimka třídy Vyjimka. Je-li více typů výjimek, které mohou být vyvrženy, oddělí se čárkou. Například double vydel() throw (Vyjimka,Jina1,Jina2); Není-li v deklaraci metody uvedeno klíčové slovo throw, znamená to, že z metody může být vyvržena JAKÁKOLIV výjimka. Pro ty, kteří znají Javu to může být trochu matoucí, protože v Javě je to přesně naopak (tedy žádná). Chceme-li v C++ deklarovat metodu, z níž nemůže být vyvržena výjimka, za deklaraci připíšeme throw (). Ještě jen zbývá dodat, že seznam výjimek, které mohou
být vyvrženy je součástí názvu metody, nebo funkce. Musí tedy být uveden i v deklaraci, i v definici. Výsledek programu je potom následující:
Odchycení jakékoliv výjimky Lze odchytit jakoukoliv vyjímku. Za klíčovým slovem catch místo typu a lokálního názvu výjimky uvedeme 3 tečky. Například: #include using namespace std; class V1 {}; class V2 {}; int main() { for(int i = 0; i < 5; i++) { try { if (i % 2 == 0) { // vyvrzeni nejake první vyjimky V1 v; throw v; } else { // vyvrzeni nejake jine vyjimky V2 v; throw v; } } catch(...) { // zachyceni jakekoliv vyjimky, zde nezname cout << "Chyceno" << endl; } } // ukonceni for cyklu } Tento příklad nedává moc smysl, ale jasně ukazuje odchycení jakékoliv výjimky. Protože při vyvržení, i odchycení výjimky dochází často ke kopírování vyvržené výjimky na zásobník, je určitě efektivnější pracovat s referencemi, nebo ukazateli na výjimky.
Vyjímky tvořící dědičnou hierarchii. C++ poskytuje používat vyjímky v situacích, kdy výjimky jsou odvozeny z jedné společné nadtřídy. Představme si třídu výjimek. Může být i abstraktní. Z této třídy dědí jiné výjimky, které jsou speciálním případem výjimky. Vytvořme si jako názornou ukázku-jednoduchou hierarchii výjimek.
Bázová třída vyjímky: #include <string> #include using namespace std; // Bazova trida vyjimky class Vyjimka { private: // Text vyjimky string Text; public: // Konstrukce vyjimky s parametrem textu Vyjimka(string s):Text(s){} // Pristupova metoda poskytnuti textu string dejText() { return Text; } }; Odvozené třídy vyjímky: // Trida vyjimky deleni nulou (odvozena od bazove tridy) class DeleniNulou : public Vyjimka { private: // Informace - ktere cislo delim nulou int Cislo; public: // Konstrukce vyjimky s parametrem textu a cisla DeleniNulou(string s, int i):Vyjimka(s),Cislo(i) {} // Pristupova metoda poskytnuti cisla int dejCislo() { return Cislo; } }; // Trida vyjimky preteceni (odvozena od bazove tridy) class Preteceni : public Vyjimka { public: // Konstrukce vyjimky s parametrem textu Preteceni(string s):Vyjimka(s) {} }; Vytvořili jsme tři výjimky. Nyní si ukážeme jak odchytávat výjimky, které mají společného předka. Problém je v tom, že musíme nejprve odchytit nejkonkrétnější výjimky a postupně odchytávat jejich nadtřídy.
Dokončím příklad: int main() { for(short int p = 5; p >= 0; p--) { // blok odchyceni vyjimek (blok kontroly) try { // test vyjimky deleni nulou if (p == 0) /* Vyvrhnu ukazatel */ throw new DeleniNulou("Deleni nulou",10); cout << "10 / " << p << " = " << 10.0 / p << endl; // test vyjimky preteceni if ((short int ) (p * 10000) < p) throw new Preteceni("Pretekl short int"); // test na obecnou vyjimku, danou bazovou tridou if (p == 3) throw new Vyjimka("Jen tak, pro ukazku"); } catch (Preteceni *v) { // Odchyceni vyjimky preteceni cout << "Pøi násobení: " << v->dejText() << endl; delete v; } catch (DeleniNulou *v) { // Odchyceni vyjimky deleni nulou cout << v->dejText() << " nelze " << v->dejCislo() << "/0" << endl; delete v; } catch (Vyjimka *v) { // Odchyceni bazove vyjimky, nadtridu odchytim az posledni cout << v->dejText() << endl; delete v; } } return 0; } V příkladu jsme vyvrhli vždy ukazatel na třídu Vyjimka, nebo na nějakého potomka této třídy. Ukazatel na tuto třídu jsme museli odchytit až jako poslední. Stačí si uvědomit, že ukazatel na potomka je zároveň také ukazatelem na předka. Znamená to, že kdyby jsme odchytávali ukazatel na třídu Vyjimka jako první, došlo by zde vždy k odchycení a další výjimky by nebyly odchyceny nikdy. Můžete se o tom sami přesvědčit, když příklad upravíte tak, že nejprve bude blok catch (Vyjimka *v) a potom ostatní odchytávací bloky. Tento problém nastává samozřejmě vždy, nejen při používání ukazatelů jako v našem příkladě.
Všimněte si v příkladě řádků delete v;. Zde vždy zlikviduji výjimku, která byla vyvržená. Často se místo vyvrhnutí ukazatelů na výjimky a odchycení ukazatelů pracuje s referencemi. Uvedu jen jednoduchý příklad: #include using namespace std; class V {}; int main() { try { V v; throw v; } catch (V &v) { cout << "Chyceno" << endl; } return 0; }
Výjimka opustí tělo funkce main(...). Nyní se podívejme, co se stane, jestliže výjimka není odchycená a opustí tělo funkce main. K tomuto účelu si napíšeme velice jednoduchý program: #include using namespace std; int main() { throw 1; /* Výjimka typu int */ cout << "Nestane se" << endl; return 0; }
V minulém článku jsem nedoporučoval používat jako výjimky primitivní datové typy, ale teď jsem to sám udělal. Snad mi to odpustíte, o typ výjimky teď vůbec nejde. Můžeme tento program zkompilovat a spustit. Uvidíme, že program se ukončí s nějakou chybovou hláškou :
Neošetřená výjimka je dost závažná chyba, proto takto "tvrdý" konec. Výjimka bude vyvržená tak, jak jsme si ukázali minule. Opustí-li výjimka tělo naší funkce main(...), dojde k zavolání funkce terminate. Tato funkce, není-li předepsáno jinak, vypíše onu chybovou hlášku a ukončí
program zavoláním funkce abort. Funkce abort se postará o to, aby volajícímu procesu byla vrácena návratová hodnota 3. Takové chování nemusí být žádoucí. Můžeme například chtít vrátit jinou návratovou hodnotu, vypsat jinou, nebo žádnou hlášku (stderr u programů pod OS Windows není). Toho lze docílit pomocí funkce set_terminate. Funkce má jako parametr ukazatel na funkci, kterou má vyvolat volání funkce terminate. Funkce terminate i set_terminate jsou deklarovány v hlavičkovém souboru exception. Ve starších překladačích se tento soubor může jmenovat jinak. Například except.h Uveďme příklad: #include <exception> #include using namespace std; void mojeTerminate() { cerr <<"Bohuzel, programator nebyl schopen osetrit vyjimky."<<endl; exit(-10); // Doporucuji vzdy ukoncit program. } int main() { set_terminate(mojeTerminate); throw 1; // vyjimka typy int cout << "Nestane se" << endl; return 0; }
Takto je chování programu v případě, že výjimka opustí funkci main(...), dáno ANSI normou. Problém nastane u aplikací pro Windows, které žádný stderr nemají. Chováni funkce terminate je jiné, a nejspíše se pro každý překladač liší. Například Borland C++ Builder vytvoří dialogové okno s hláškou: "Abnormal program termination". Budete-li ale chtít experimentovat s výjimkami v GUI aplikaci, zjistíte, že všechny výjimky vyvržené z metod formulářů jsou ošetřeny nějakým mechanizmem, který je pro programátora "neviditelný". Chcete-li si zkusit v GUI aplikaci v BCPPB vyvrhnout výjimku z funkce WinMain (obdoba main), musíte výjimku vyvrhnout přímo v této funkci. Pokud možno mimo blok try. U GUI aplikací v OS Linux tento problém nenastává, protože v Unixových systémech mají i GUI aplikace svůj standardní chybový výstup.
Vyvržení nedeklarované vyjímky. Je-li z těla funkce nebo metody vyvržená výjimka, která není v seznamu výjimek, které mohou být z těla funkce, nebo metody vyvrženy, dojde k zavolání funkce unexpected. Tato funkce implicitně zavolá funkci terminate. Chování funkce unexpected lze změnit pomocí funkce set_unexpected, která má jako svůj parametr ukazatel na funkci bez parametrů a vracející void. Použití funkce set_unexpected je obdobné jako použití funkce set_terminate. Funkce unexpected je deklarována v souboru exception.
Příklad: #include <exception> #include using namespace std; void mojeUnexpected() { cerr << "Bohuzel, programator spatne pracuje s vyjimkami" << endl; exit(-10); } void f() throw() { throw 1; } int main() { set_unexpected(mojeUnexpected); f(); return 0; }
Standardní vyjímky. V standardní knihovně C++ se výjimky příliš nepoužívají. Informace o chybě se často předává jako předem určená návratová hodnota z funkce, nebo metody, nebo pomocí chybového stavu - viz třeba objekt cin, nebo jiný objekt třídy istream, se kterým nelze pracovat v případě, že přečte chybná data. Několik standardních výjimek v C++ ale přece jenom je. Budeme se k nim postupně dostávat v dalších článcích. Všechny tyto výjimky jsou děděny ze třídy exception (např. ve Visual C++): class exception { public: exception() throw(); exception(const exception& rhs) throw(); exception& operator=(const exception& rhs) throw(); virtual ~exception() throw(); virtual const char *what() const throw(); };
Jedna výjimka, o které by jsme už ale měli vědět je výjimka typu bad_alloc, nebo nějaký její potomek, kterou vyvrhne operátor new v případě, že selže. V některých starších překladačích, které neodpovídají normě, se setkáme s výjimkou typu xalloc. Výjimka bad_alloc je definována v prostoru jmen std. Operátor new v případě selhání může buď vrátit NULL, nebo vyvrhnout tuto výjimku. Operátor new může selhat v situaci, kdy nelze alokovat požadovanou paměť.
Úplné ošetření činnosti operátoru new je následující: { int *pointer; try { if ((pointer = new int[3]) == NULL) { // Prece jenom, pro jistotu pokud new operator // vrati pouze NULL (zalezi na prekladacich) cout << "Vratil NULL" << endl; } } catch (std::bad_alloc &b) { cout << "Vyhodil vyjimku" << endl; } } }
Jak se chová new, když selže, se můžete přesvědčit například tak, že v tomto příkladě místo čísla 3 (velikost pole) dáte hodně velké číslo. Chcete-li možnost vyvržení výjimky u operátoru new potlačit, lze to pomoci parametru operátoru new, který je nothrow: { int *i; // Urcite new nevyvrhne vyjimku. i = new(nothrow) int[10]; }
Nevyvrhne-li výjimku operátor new, neznamená to samozřejmě, že nějaká nemůže být vyvržena z konstruktoru, který bude zavolán. Výjimka bad_alloc, i parametr nothrow (Je to vlastně prázdná struktura.) se vztahují pouze na selhání alokace paměti.
Závěrem k vyjímkám. Výjimky jsou určitě skvělá a pohodlná věc. Vše ale něco stojí. Při používání výjimek platíme tu největší daň za používání objektově orientovanému programování. Výsledný program používající výjimky bude jednak o mnoho větší a také mnohem pomalejší. Bude pomalejší dokonce i tehdy, nebude-li žádná výjimka vyvržená. Stačí pouze, že existuje blok try. Uvědomme si, co vše se musí v hlídacím bloku kontrolovat. To vše stojí čas a také jsou k tomu potřebné instrukce navíc. Záleží-li Vám opravdu na rychlosti programu, potom je používání výjimek na pováženou. Naopak ale například v programovacím jazyce Java se to výjimkami jen "hemží" a s rychlostí si nikdo nedělá starosti. Stejně tak podíváme-li se například na funkci WinMain v BCPPB, zjistíme, že celé její tělo je vlastně v hlídacím bloku. Používání výjimek je sice drahé, ale pro programátora velmi užitečné.
Cvičení. Cv. 1. Napište svou obecnou bázovou třídu pro vyjímky. Výjimka poskytuje informaci o času vyvrhnutí vyjímky, o programu vyvrhnutí vyjímky, o kódu vyjímky (pokud neznámý, tak 0xFFFF) a o textu vyjímky. Definice virtuálních metod je provedeno v abstraktní třídě. Bázová třída obsahuje defaultní konstruktor, konstruktor s optimálními argumenty, přístupové metody s optimálními argumenty. Zvažte, které argumenty musíte předat třídě, které si třída vytáhne sama!! Abstraktní třída se jmenuje AbExcept, bázová třída se jmenuje BaseExcept.
Cv. 2. Napište program pro opakované zadávání jména a příjmení, RČ. Jméno má maximálně 15 znaků, příjmení 20 znaků. Program ošetřete vy jímkami pro případ nesmyslných dat (dlouhé jméno, syntaxe RČ). Použijte Třídu vyjímky ze Cv. 1.
Cv. 3. Vytvořte strukturu MyData, která obsahuje dynamické alokace řetězců. Struktura má defaultní konstruktor, funkci Init, virtuální destruktor. Funkce Init a defaultní konstruktor inicializuje všechny atributy na 0, včetně ukazatelů na dynamicky alokovatelné řetězce. Po inicializaci instance struktury, proveďte konstrukci std objektu string pomocí argumentu řetězce (v té době ukazujících na 0). Vyzkoušejte zda bude vyvrhnuta nějaká výjimka, pokud ano jaká, pokud lze něco zobrazit, zobrazte jako informaci.