Bredyho blog - Ondřej Novák

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).

vytvořeno: 27.12.2009 22:35:54, změněno: 27.12.2009 22:35:54
Jsou informace v článku pro Vás užitečné?
  • (2)
  • (0)
  • (0)
  • (0)
  • (0)
Nick nebo OpenID
Vzkaz
 
28.3.2016 11:44:02

Gucci Handbags

Features: combined with personal body characteristics
Gucci Handbags http://www.charopf.com/gucci-outlet/
28.3.2016 10:23:16

Gucci Shoes

itssss great varieties......... simplest.......!!!! thanks a whole lot to get sharing as well as economizing everyone
30.7.2010 14:31:16

Sten

what() vrací const char*, protože:
1) je i v std::bad_alloc. Jak chcete alokovat std::string, když není paměť?
2) musí být exception safe. Vracení std::stringu takové není, vyvolává copy constructor (opět std::bad_alloc). Můžete dokonce své výjimky udělat tak, že se pokusí zformátovat, co obsahují, ale pokud se jim to nepovede, vrátí nějaký statický popisek

Žádná paměť nikde nelítá. what() vrací paměť, která je alokována někde uvnitř výjimky, takže výjimka ji v destruktoru má uvolnit (případně jde o statickou paměť). Při výpisu do std::cout se dokonce žádná paměť nikde nealokuje, takže tam jde vypsat i std::bad_alloc, když dojde paměť

Jediný možný problém je tedy Unicode a zjištění místa programu, kde k výjimce došlo, tam what() není dostatečné
22.5.2010 22:26:29

Ondra openid

Neříkám, že by what() mělo být alfou omegou výjimkového systému. Říkám jen, že není vhodné ho úplně vynechat. Že každá výjimka by měla poskytnout informaci o tom, co představuje i přes what(). Jako pro zpětnou kompatibilitu. Můj výjimkový systém vychází ze std::exception ale pokračuje přes IException, který přidává další funkce, jenž umí například výjimku "znovu vyhodit", nebo umí získat víc informací o výjimce, a třeba popis chyby vyhazují jako řetězec (string) objekt. To dědí třída Exception, která implementuje většinu věcí z obou rozhraní, podporuje i "reasons", neboli řetězení výjímek, kdy vzniklá výjimka jako reakci na jinou výjimku zařadí původní výjimku do seznamu důvodů. A každá výjimka se umí i naklonovat. A z Exception pak virtuálně dědí nové "rooty", jako typy výjimek, třeba SystemException, nebo IOException, a z nich pak dědí jednotlivé výjimky. Nepoužívám rozlišení typu pomocí kódu nebo enumu. Každá výjimka je dána svým typem. Ten lze i nepřímo získat pomocí funkce typeid(), ale lze získat i identifikátor výjimky a její popis. Číselné chyby a kódy jsem zavrhl, nejsou dost obecné.

Ale bych se vrátil, samozřejmě že není dobré stavět na what(), ale je dobré jej aspoň podporovat, protože když už nic jiného, tak aspoň ten what() je schopno něco o neznámé výjimce říct. Není nic horšího, než když program ohlasí "unknown exception". Moje whaty většinou řeknou něco v tom smyslu kde vyjímka vznikla, jaký má popis a pokud má výjimka připojený důvod, tak do popisu zahrnou i seznam důvodů. Pak ten what může být i na několik řádek.

Pokud byste chtěl tohle vidět v praxi, tak si stáhněte třeba Seznam Pošťák, nebo Seznam Lištičku (www.listicka.cz), a v těch programech udělejte nějaký chybový stav, třeba je spusťte s vytaženým ethernetem. A pak se podívejte do Nastavení, Pokročilé, Chyby. Vše co má červený křížek je většinou obsah what() při odchycení výjimky.
18.5.2010 03:15:36

Miloslav Ponkrác

Řeknu to upřímně, ne vše co STL dělá dobře dělá.

Velký problém u C++ je, že trvá dlouhou dobu, než se odnaučíte C a začnete programovat jako v C++.

STL vznikala v době, kdy za sebou zanechala ještě řadu Céčkovských manýrů a what() je jedním z nich.

Já ve svých kódech výjimky std::exception nechytám kromě hlavního filtru v main(). Není proč. Kromě výjimek std::bad_alloc a std::bad_cast žádné jiné výjimky nasyrovo od std::exception nevznikají.

Nehledě na to, že už opravdu nechci někam posílat strojákové pole bajtů, které what() vrací. Pokud by byl v C++ standardně garbage collector ještě možná by se to dalo skousnout. Ale takhle se musíte starat ještě o další paměť, která vám nekontrolovatelně lítá po programu.

Druhá věc je, že v mých kódech už je všude Unicode. Co vrátíte v const char * řetězci what(), když budete vyhazovat výjimky typu „file abcdefgh.txt not found“ když název souboru bude tvrdě v Unicode a nepřeveditelný do 8mi bitové znakové sady?

Osobně prostě what() beru jako obsolete a deprecated. what() je špatné API pro vracení chyb a hloupé.

Text výjimky program stejně vůbec nepoužívá, chytají se pomocí typu, či zjemňují pomocí číselných kódů typu enum. Text výjimky se maximálně loguje, nebo zobrazuje uživateli.

Ale vše jsou mé subjektivní názory.

Pokoušel jsem se kdysi a úspěšně udělat univerzální what() funkci. Fungovalo to nádherně, ale pak jsme ho smazal, protože to nemá smysl. Stejně jako Vy jsem při what() inicializoval řetězec pro what(). Základní problém je, že ale what() má tolik nedostatků, že nemá smysl to řešit.
12.5.2010 21:03:43

Ondra openid

S tím what() to není úplně dobrý nápad. Já se snažím vracet i ve what() celý text vyjimky. Kolikrát totiž na základní úrovni vypadne vyjimka, o které neumím říct nic jiného, než what().

Funguje to tak, že součástí instance výjimky je proměnná řetězce s příznakem mutable, která se při prvním zavolání what() inicializuje textem výjimky.

O výjímkách obecně a o LightSpeed::Exception, což je můj výjimkový systém v knihovně LightSpeed napíšu ještě nějaký článek.

Díky za reakci.
12.5.2010 04:09:42

Miloslav Ponkrác

Hypotéza ohledně DLL je správná. Protože C++ nikdy nezaručuje, že je možné počítat s něčím „za branami“.

Podle standardu C++ throw způsobí vytvoření dočasné proměnné, není specifikováno kde tato proměnná vznikne. Rethrow pomocí throw bez parametrů způsobí jen „znovuoživení“ této dočasné proměnné.

DLL i program tedy může čekat a ukládat tuto dočasnou proměnnou jinde a na jiných místech, tudíž si je vzájemně nemusí vidět.

Celý mechanismus házení a chytání výjimek je vnitřně docela složitý. Daleko složitější, než by vyplývalo z jednoduchého popisu v C++. C++ tam trikuje, a odhaduje co se dá ošulit, aby celý mechanismus výjimek byl rychlý a efektivní.

Ovšem pochopní řady jevů kolem výjimek by chtělo důkladnější popis vnitřních mechanismů a implementačních triků.

Dodatek: Co považuji v std::exception za nešťastné je právě ta metoda what(). Myslel jsem si, že v době vyšších jazyků už nikoho nenapdne vracet statický pointer na znaky, ale bohužel. Sám metodě what() předávám chybovou zprávu "Use better method xy() to get message". A lepší metoda vrátí objekt typu string, nebo něco co není návratem do doby strojáku jako u metody what().
1.3.2010 19:32:32

Ondra openid

Doplněk k článku. Budete-li toto používat k delegování výjimek vyhozených z jiného DLL, můžete narazit na zajímavé jevy. Například vyhozená výjimka přes throw; mající původ v jiném DLL nebude odchycena v žádnem bloku try-catch, dokonce nezareaguje ani na tři tečky. Odchytne jí až catch v místě, kde se řízení vrátí do původního DLL. Zajímavé ale je, že zásobník se poctive uklidí, včetně volání destruktorů. Hypotéza je, že šňůra catch handlerů je organizovaná per DLL a při vracení z obsluhy do cizího DLL prostě výjimka nevidí handlery deklarované na straně obsluhy.

Podobné články

C++ - Akce a Zpráva jako objekt

Akcí nazývám obyčejné volání funkce, zprávou pak volání metody objektu (jak je chápáno podle OOP). Lze vůbec volání čehokoliv reprezentovat jako objekt? A k čemu je to vlastně dobré?

Pár triků na kompresi přímo v kódu

Pomocné struktury a data lze komprimovat přímo v kódu. Jak na to. A má to vůbec smysl?

Serializace dat a objektů

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

Multithreading v C++ (ve Win32) III - OOP a vlákna

Pokračování dokumentace k Multithreading v C++ (ve Win32) - Tentokrát si povíme něco o tom jak začlenit vlákna do OOP

FastAllocPool - urychlení častých alokací a dealokací

Pokud v programu z nějakých důvodů potřebujeme často provádět new a delete nad některými třídami, můžeme zvýšit efektivitu těchto operací zavedením poolu předalokované paměti
Reklama: