Delegování zpracování výjimky
V situacích, kdy potřebujeme určité typy výjemek zpracovat stejným způsobem, můžeme jejich zpracování delegovat do jiné funkce.
Příklad:
//Blok s výjímkou
try {
//... operace
} catch (Except1 e) {
// zpracování výjimky Except1
} catch (Except2 e) {
// zpracování výjimky Except2
} catch (std::exception e) {
// zpracování std::exception
} catch (...) {
// zpracování ostatních výjimek
}
Pokud v programu existuje vícero míst, které potřebujeme takto ošetřit, velmi často si pomůžeme makrem
#define CATCHALL catch (Except1 e) { ... } \
catch (Except2 e) { ... } \
catch (std::exception e) { ... } \
catch (...) { ... }
Makro CATCHALL pak vkládáme všude tam, kde potřebujeme tuto společnou obsluhu.
Není to úplně C++ a neřeší to všechny problémy. Například pokud potřebujeme někde ošetřit některé vyjmenované výjimky nějak speciálně, musíme vnořit dva try-catch bloky do sebe.
Určitě správným řešením je ukotvit všechny výjimky na společného předka, třeba tak, aby všechny výjimky dědily například std::exception. Pak je lze odchytnout výjimky na jejím společném předku.
Je to částečné řešení, ale určitě doporučuji. Už proto, že výjimka typu std::exception musí implementovat metodu what(), jenž by měla v lidském jazyce (ideálně anglicky) popsat důvod vzniku výjimky. Zjednodušuje to zejména situace vzniklé v případě, že je hozena výjimka v místě, kde není ošetřena. Pak na nejvyšší úrovni má program možnost takovou výjimku zalogovat a tak sdělit uživateli, či vývojaři nějakou informaci o tom, co se stalo.
Z hlediska dalšího zpracování doporučuji odchytávat výjimku jako referenci. Umožňuje to například pozdější přetypování na potomka.
//Blok s výjímkou
try {
//... operace
} catch (std::exception &e) {
processException(e);
} catch (...) {
// zpracování ostatních výjimek
}
V příkladu je vidět, jakým způsobem lze zpracování výjimky delegovat do funkce processException(), která posléze může pomocí testů přes dynamic_cast určit typ výjimky a zajistit další zpracování.
void processException(std::exception &e) {
Except1 *e1;
Except2 *e2;
if ((e1 = dynamic_cast<Except1 *>(&e)) != 0) {
// zpracuj výjimku Except1
} else if ((e2 = dynamic_cast<Except2 *>(&e)) != 0) {
// zpracuj výjimku Except2
} else {
//zpracuj std::exception
}
}
Pokud to porovnáme s makrem, zjistíme, že výkonově na tom nebudeme o moc hůře, pakliže si uvědomíme, že podobné testování se provádí interně při hledání catch bloku. Ať už tedy odchytneme výjimky makrem, nebo ji odchytneme na společném předkovy a následně třídíme pomocí dynamic_cast<>, u makra si polepšíme jen o jedno volání dynamic_cast<> což není zas tak velký problém, pokud porovnáme s náročoností vlastního zpracování výjimky.
Z hlediska C++ je to určitě o dost lepší. Kód je rozhodně přehlednější a troufám si říct, že i flexibilnější. Navíc můžeme snadno na některých místech definovat pro některé typy výjimek jiné zpracování. Jednoduše je uvedeme do catch bloku před std::exception.
Výše uvedený způsob stále vyžaduje dva catch bloky, a to může být pro některé líne vývojáře problém. Tím druhým blokem je totiž třítečkový catch, který ošetřujeme samostatně, protože výjimku nemůžeme poslat jako parametr. V tomto směru se blýská na lepší časy až v C++0x, kde je možné i takovou výjimku uložit do proměnné a pak s ní dále pracovat, či dokonce zjistit její typ.
Problém se dvěma catch bloky lze řešit i tak, že třítečkovou výjimku nebudeme odchytávat. Většinou totiž stejně neumíme takovéto výjimky zpracovat a tak je posíláme výše. Avšak jen do té doby, dokud to je možné. Pokud například catch větev obsahuje kód pro rollback operace nebo nahrazuje chybějící 'finalize' , vynechávat nesmíme.
FILE *f = fopen(...);
try {
ctiSoubor(f);
fclose(f);
} catch (...) {
fclose(f);
throw;
}
V příkladě výjimka vzniká ve funkci ctiSoubor. Protože chceme výjimku propagovat výše, avšak zároveň musíme zajistit uzavření otevřeného souboru, musíme definovat třítečkový catch a v něm, před vyhozením výjimky výše, uzavřít otevřený soubor.
Zabránit případného opakování lze také vnořením více try bloků do sebe.
FILE *f = fopen(...);
try {
try {
ctiSoubor(f);
fclose(f);
} catch (...) {
fclose(f);
throw;
}
} catch (Except1 e) {
//...
} catch (Except2 e) {
//...
} catch...
Elegantní řešení, jak delegovat zpracování výjimky do funkce je využít možnost vstoupit do nového try-catch bloku při zpracování nadřazeného catch bloku, a v něm zavolat "prázdný" throw (tedy throw bez parametrů).
Pro zopakování; příkaz throw bez parametrů způsobí "rethrow" právě zpracované výjimky, tak jak byla původně hozena, bez ohledu na způsob jejího chycení. A výhodné je, že funguje i u třítečkového catch bloku, čehož jsme využili v předchozím příkladě. Zajimavé určitě je, že toto lze udělat i v případě, že vrámci catch bloku zřídíme nový try-catch block. Vypadá to zhruba takto:
try {
funkceHodiVyjimku();
} catch (...) {
try {
throw;
} catch (E1 e1) {
zpracujE1(e1);
} catch (E2 e2) {
zpracujE2(e2);
} catch (E3 e3) {
zpracujE3(e3);
}
}
Tohoto mechanismu lze elegantně využit, pokud nově zřízený try-catch block umístíme do společné funkce.
try {
funkceHodiVyjimku();
} catch (...) {
onException();
}
void onException() {
try {
throw;
} catch (E1 e1) {
zpracujE1(e1);
} catch (E2 e2) {
zpracujE2(e2);
} catch (E3 e3) {
zpracujE3(e3);
}
}
Fantasii se meze nekladou a bystrý čtenář jistě našel další využití pro takovou konstrukci. V případě objektů, které volají uživatelské funkce pomocí rozhraní (typicky plně abstraktní třídy) v nichž může nastat výjimka, mají tyto objekty v rozhraní k dispozici funkci onException(), kterou zavolají v případě, že zachytí výjimku vyhozenou z uživatelské funkce. Objekt, který uživatelské funkce implementuje pak může implementovat i reakci na neodchycenou výjimku, aniž by ji volající musel znát. (což je o 100% lepší řešení, než když výjimka skončí v unexpected() )
Jako příklad uvedu třídu App ve frameworku LightSpeed. Tato třída představuje abstraktní aplikaci, a je zároveň singleton. Pokud nastane výjimka v aplikaci, propadne až na nejvyšší úroveň, kde sídlí framework, který aplikaci spustil. Aby aplikace dostala šanci zpracovat neodchycenou výjimku, implementuje funkci onException, kde si může zřídit výše popsaný try-catch block a hozenou výjimku tak snadno zatřídit do příslušného catch bloku. Vlastní framework přitom vůbec nemusí výjimky znát, a není ani potřeba, aby měly společného předka.
Z hlediska výkonu je uvedené řešení o něco pomalejší, než výse popsané způsoby. Vyžaduje totiž jeden throw navíc, čímž může zpracování výjimky zpomalit. Na druhou stranu, počítá se s tím, že takovéto výjimky nebudou vznikat často, nebo spíš vůbec a pokud vzniknou, budou spíš informovat o chybě. Obecně poslání výjimek není v tom, aby nahrazovaly běžný programátorské postupy, ale řešily výjimečné situace, tedy situace, kde spíš než o výkon jde o stabilitu aplikace, a v těch nejkrajnějších případech o řízené selhání, které poskytne dostatek informací o vzniklé výjímečné situaci, která selhání způsobila. (tedy namísto suchého konstatování "core dumped", či oblíbené SIGSEG).

Následující článek je úvodem do další série o generickém programování (šablony), nyní se zaměříme na problém perzistentního ukládání dat nebo jejich transport, obecně o serializaci a deserializaci dat