Kurz C++ (18.)

Dnes budeme pokračovat výjimkami, které jsme načali v minulém dílu. Probereme podrobněji šíření výjimek jazyka C++, standardní výjimky knihovny jazyka C++ a řekneme si v jakých funkcích ze standardní knihovny mohou vznikat.

18.1. Výjimky - dokončení

18.1.1. Šíření výjimek

    V minulém dílu jsme si předvedli, jak pomocí klíčového slova try můžeme vyvolat takzvanou synchronní výjimku. Dále víme, že blok catch (tzv. handler) může tuto výjimku zachytit a nějakým způsobem na ni zareagovat. Provedením bloku catch výjimku takzvaně obsloužíme. Z názvu synchronní výjimka lze vytušit, že existují i výjimky asynchronní. Asynchronní (hardwareová) výjimka může být generována libovolnou instrukcí. V jazyce C++ nelze jako výjimku zachytit například klávesovou kombinaci Ctrl + Break, která vyvolá přerušení.

    Nyní si povíme něco podrobněji o šíření výjimky. Po vzniku výjimky dojde k přeskočení řádků po příkazu throw a program začne hledat handler, který by odpovídal typu objektu, který byl pomocí příkazu "vyhozen". Avšak těsně před opuštěním bloku try, ve kterém výjimka vznikla, dojde k zavolání destruktorů všech lokálních instancí objektových typů v paměťové třídě auto. Pokud funkce nalezne odpovídající handler, pak do něj vstoupí a je na handleru, co s výjimkou udělá - může ukončit program, vypsat chybové hlášení atd. Pokud handler program neukončí, program pokračuje za ukončovací závorkou tohoto handleru. V minulém dílu jsme si ale ukázali, že za blokem try může následovat více handlerů. V tom případě pak program přeskočí všechny ostatní handlery a pokračuje za ukončovací závorkou posledního handleru. Pokud je hledání handleru neúspěšné, pak dojde k rozšíření výjimky do vyššího bloku. Vyšší blokem může být např. funkce, která zavolala funkci, ve které vznikla výjimka. Příkazy za voláním funkce v tomto nadřízeném bloku se opět neprovedou a dojde k novému hledání handleru. Pokud se program dostane na nejvyšší úroveň, kterou je funkce main(), pak dojde k zavolání funkce terminate(), která program okamžitě ukončí. Důležité je, že při opouštění bloků se provádí úklid, jak bylo uvedeno. V případě zavolání terminate() k ničemu takovému nedojde. Pokud je program ukončen touto funkcí, mluvíme o tzv. neošetřené výjimce. Dost bylo teorie, ukažme si trošku složitější příklad:

#include <iostream.h>
#include <math.h>

class Vysl {
private:
    double m_arRes[2];
public:
    Vysl(double _fVysl1, double _fVysl2)
    {
        m_arRes[0] = _fVysl1;
        m_arRes[1] = _fVysl2;
    }

    double GetVysl1() { return m_arRes[0]; }
    double GetVysl2() { return m_arRes[1]; }
};

double Odmocnina(double a)
{
    try
    {
        if(a < 0) {
            throw "Nelze spocitat odmocninu ze zaporneho cisla";
        }

        return sqrt(a);
    }
    catch(char *)
    {
        throw;
    }
    catch(...)
    {
        cout << "Nastala neznama chyba zachycena funkci Odmocnina" << endl;
    }
}

void ZadejCislo()
{
    try {
        double a, b, c, odmdiskr;

        cout << "Zadej koef kvadr. rovnice (ax^2 + bx + c) : " << endl;
        cout << "\ta = ";
        cin >> a;
        cout << "\tb = ";
        cin >> b;
        cout << "\tc = ";
        cin >> c;

        cout << "Reseni : " << endl;

        odmdiskr = Odmocnina(b*b - 4*a*c);

        if(0 == odmdiskr)
        {
            throw (-b) / (2*a);
        }
        else
        {
            throw(Vysl((-b + odmdiskr) / (2*a), (-b - odmdiskr) / (2*a)));
        }
    }
    catch(char *e)
    {
        cout << "Nema reseni v oboru realnych cisel (nastala chyba " << e << ")" << endl;
    }
    catch(double vysl)
    {
        cout << "Ma jedno reseni a to x1 = " << vysl << endl;
    }
    catch(Vysl& e)
    {
        cout << "Ma dve reseni a to x1 = " << e.GetVysl1() << " a x2 = " << e.GetVysl2() << endl;
    }
    catch(...)
    {
        cout << "Nastala nejaka jina chyba" << endl;
    }
}

int main(int argc, char* argv[])
{
    ZadejCislo();

    return 0;
}

    Nyní si projdeme všechny tři možnosti, které mohou při běhu programu nastat. Přeskočíme tělo funkce ZadejCislo(), protože v něm není nic zajímavého kromě klíčového slova try, které uvozuje pokusný blok ve kterém se zachytávají výjimky :

1.    Nejjednodušší případ, kdy uživatel zadá rovnici, kterou nelze vyřešit v oboru reálných čísel (tedy diskriminant je záporné číslo, ze kterého nelze spočítat odmocninu):

    Ve funkci odmocnina dojde ke splnění podmínky a tedy je vyvolána výjimka typu char* a začne hledání handleru, který by tuto výjimku obsloužil. Při výstupu z bloku try by se uvolnily lokální proměnné včetně zavolání destruktorů, ale zde žádné nejsou. Handler je nalezen, takže dojde k provedení jeho těla. V těle handleru se nachází novinka a tou je samostatné klíčové slovo throw. To způsobí, že výjimka je dále šířena do nadřazeného bloku. To nám dává možnost výjimku částečně ošetřit (např. uzavřít některé soubory apod.) a šířit ji dál. Handler ... (ellipsis) je uveden jen aby bylo vidět, že blok try může mít více handlerů. Z této funkce se ale jiná výjimka šířit nemůže. Vraťme se k naší šířící se výjimce typu char*. Ta se rozšířila do bloku try který zavolal funkci Odmocnina(). Další příkazy za funkcí Odmocnina() se neprovedou a začne se hledat handler pro obsluhu výjimek typu char*. Ten je nalezen a vypíše řetězec, že kvadratická funkce nemá řešení a důvod, kterým je, že nemůžeme odmocnit záporné číslo. Protože handler neukončil program, program pokračuje za závorkou posledního handleru ve funkci ZadejCislo() a dojde tedy k ukončení této funkce a návratu do funkce main().

2.    Diskriminant rovnice je 0, pak má rovnice pouze jedno řešení:

    Funkce Odmocnina() proběhne v tomto případě bez vyvolání výjimky, ale výjimka je vyvolána o dva řadky později. Nyní je výjimka typu double a proto se hledá handler, který je schopný tento typ obsloužit. První šanci dostane handler (char *e), ale typ neodpovídá a tak je přeskočen, druhý handler vyhovuje a tak program vypíše řešení kvadratické rovnice. Zde je nutné poznamenat, že pokud bychom na prvním místě uvedli handler ..., pak by žádný jiný handler neměl šanci vzniklou výjimku zachytit. Program končí stejně jako v bodě 1.

3. Diskriminant je kladný a rovnice má pak dva kořeny:

    Funkce Odmocnina() opět proběhne správně, ale výjimka je vyvolána o pár řádků dále. Klíčové slovo throw nám umožňuje "vyhodit" i objektový typ, čehož také využijeme. Zkonstruujeme instanci třídy Vysl, která obsahuje obě řešení rovnice. Opět dojde k hledání handleru a je nalezen handler s typem reference na třídu Vysl. Tím dojde k předání adresy, což je výhodné u rozměrnějších objektů. Ostatní handlery jsou opět přeskočeny a program končí stejně jako v předchozích bodech.

    Je nutné si uvědomit, že výše uvedený příklad je jen pro výukové účely, v reálu jde samozřejmě napsat jako pár if příkazů. Obsluha výjimek totiž není zadarmo a tak je nutné vždy zvážit, jestli nelze program napsat lépe.

18.1.2. Výběr handleru podrobněji

    Jak už jsme si uvedli, handler se vybírá podle typu výjimky. Typ výjimky je určen typem hodnoty, kterou jsme předali při vyvolání výjimky throw.

-    Výjimku můžeme zachytit buď přímo pomocí typu výjimky nebo pomocí reference (např. Typ& e) na typ výjimky.

-    Jestliže vyhozeným typem je třída, která má rodičovskou třídu/třídy, pak ji lze zachytit i handlerem, který bere jako typ rodičovskou třídu. Také ji lze zachytit pomocí reference na základní třídu. Poznamenejme jen, že pokud je objekt zachycen pomocí reference, pak není kopírován, ale je přímo svázán s objektem, který jsme specifikovali při použití throw. Jinak je to lokální kopie.

- Pokud uvedeme rodičovskou třídu objektu před odvozenou třídou v seznamu handlerů, pak při vyhození odvozené třídy dojde k jejímu zachycení handlerem pro rodičovskou třídu, ačkoliv dále existuje handler přesně pro tento typ

- handler s parametrem TypA* zachytí i výjimku typu TypB*, ale jen v případě, že existuje standardní konverze ukazatele typu TypA* na TypB*

- handler ... (ellipsis) zachytí všechny typy výjimek. Je proto nutné ho uvést jako poslední

    Například typ char* je zachycen handlerem na typ void*, neboť tato konverze je automatická. Obráceně to neplatí. Dále se neprovádějí třeba konverze typu char na int, ačkoliv jinde jsou úplně běžné.

    Ještě je dobré zmínit, že pokud se hledá handler pro výjimku, nesmí vzniknout výjimka jiná (na stejné úrovni). To je důležité především, pokud se při opouštění bloků try volají destruktory lokálních objektů. Z toho důvodu se doporučuje psát destruktory tak, aby se z nich nemohly šířit výjimky.

18.1.3. Funkce vyhazující výjimky

    Pokud se výjimka šíří z již napsané funkce a na projektu pracuje více programátorů, nebo není přístup ke zdrojovému kódu funkce, pak je vhodné uvést jaké výjimky se mohou z dané funkce rozšířit. Samozřejmostí je, že by to mělo být uvedené v komentáři k funkci, ale jazyk C++ nabízí následující zápis:

NavratHodnota JmenoFunkce(Parametry) throw (seznam výjimek, které se z funkce mohou šířít);

    Pokud není u hlavičky funkce uveden seznam výjimek, které se z ní mohou rozšířit, pak se předpokládá, že se z ní může šířit jakákoliv výjimka. Pokud jsou uvedeny, pak se smějí šířit jen uvedené typy výjimek. Protože typem výjimky může být i objektový typ, pak jsou povoleni i potomci této třídy. Poslední možností je uvedení klíčového slova throw bez seznamu výjimek. Potom se z dané funkce nesmí šířit výjimka žádná. Následují příklady:

int VsechnyVyjimky();    // standardni zapis, muze se sirit cokoliv

int ZadnaVyjimka() throw();   // nesmi se sirit zadna vyjimka mimo telo funkce

int VybraneVyjimka() throw(int, Vysl);    // mohou se sirit pouze uvedene vyjimky

    To, že se z funkce nesmí šířit výjimka neznamená, že v ní výjimka nemůže vzniknout. Jen musí být ošetřená uvnitř této funkce.

    Pokud se z funkce rozšíří výjimka, která nemá "povolení" se z funkce šířit, pak způsobí zavolání funkce unexpected(), která implicitně zavolá funkci terminate() a ta, jak už víme, ukončí program. Podobně jako funkci terminate(), můžeme předefinovat i funkci unexpected() a to pomocí funkce set_unexpected().

    Překladač C++ obsažený ve vývojovém prostředí Visual Studio .NET firmy Microsoft bere možnost, že se z funkce může rozšířit výjimka určitého typu stejně, jako že se z této funkce může šířit výjimka jakéhokoliv typu.

18.1.4. Standardní výjimky jazyka C++

    Standardní knihovna jazyka C++ nám nabízí k využití hierarchii tříd výjimek. Pro využití těchto tříd je nutné vložit do programu soubor <stdexcept> (opravdu bez přípony .h). Předkem této hierarchie je třída exception, která je definována v hlavičkovém souboru <exception>.

    Objektový typ exception je součástí prostoru jmen std a proto bychom při přístupu k němu a ke standardním, od něj odvozeným, výjimkám měli použít zápis std::exception. Od třídy exception dědí všechny odvozené třídy virtuální metodu what(), která vypíše řetězec obsahující chybu. Od třídy exception jsou odvozeny například třídy runtime_error a logic_error, které mají konstruktory s parametrem typu string, který umožňuje zadat řetězec s důvodem chyby.

    Pokud budeme zachytávat výjimky ze standardní knihovny handlerem catch(std::exception& e), potom zachytíme všechny třídy z této hierarchie. Zavoláním e.what() uvnitř handleru dostaneme informaci o vzniklé výjimce.

    Výjimky typů z této hierarchie vyvolávají některé funkce a metody objektových typů ze standardní knihovny jazyka C++.

18.1.3. Funkce ze standardní knihovny vyvolávající standardní výjimky

    Zde si zmíníme pár funkcí, které způsobí v případě problému vyvolání výjimky.

    - v C++ lze za běhu programu určovat typ instance pomocí operátoru typeid. Tento operátor při nesprávném použití, např. když se snažíme zjistit informace o typu, na který ukazuje pointer s hodnotou NULL, vyvolá výjimku bad_typeid:

#include <iostream.h>
#include <exception>
#include <typeinfo>

class A { virtual f() {}; };

int main () {
    try {
        A* a = NULL;
        typeid (*a);
    }
    catch (std::exception& e)   
// chytame vse
    {
        cout << "Exception: " << e.what();
    }
    return 0;
}

     Program vypíše chybovou hlášku jen v případě, že je to ladicí (debug) verze. Ve finální (release) vypíše chybu Abnormal program termination.

- pokud se operátoru new nepodaří přidělit paměť, pak vyvolá výjimku typu bad_alloc

- výjimka ios_base::failure (ios_base je prostor jmen) může být vyvolána v případě neúspěchu vstupní nebo výstupní operace objektového datového proudu. Objektové proudy ale negenerují výjimky implicitně, je nutné zavolat funkci ios_base::exceptions()

- některé operace přetypování pomocí operátoru dynamic_cast mohou vyvolat výjimku bad_cast.

18.2. Co bude příště?

    V dnešním dílu jsme dokončili obsluhu výjimek C++. Na závěr je nutné říct, že tyto výjimky nejsou jedinými, se kterými se lze setkat. Prostředí Windows nabízí tzv. strukturovanou obsluhu výjimek, knihovna MFC (Microsoft Foundation Classes - soubor tříd zjednodušující vývoj Win32 aplikací) nabízí svojí vlastní úpravu obsluhy výjimek jazyka C++. Také jsme se seznámili s několika novými pojmy jako např. prostory jmen. Některé z těchto budou objasněny v dalším dílu, kde se zaměříme i na některé speciální operátory přetypování, identifikaci typu za běhu programu (RTTI) a možná i něco víc.

Příště nashledanou.

Ondřej Burišin