Znám dost kolegů programátorů (a internetové diskuze jsou jimi plné), kteří nelibě nesou neexistence klíčového slova finally v bloku try-catch.
Blok uvozený klíčovým slovem finally se provadí vždy, bez ohledu, zda blok try skončil nebo neskončil výjimkou. Takový blok najdeme v Javě a hodně se využívá.
Argumentem, proč „ne“ finally v C++ je ten, že všechny proměnné deklarované uvnitř bloku try by měly dodržovat RAII idiom. Teoreticky je tedy blok finally zbytečný, praxe však bývá poněkud odlišná. Leckdy totiž převod algoritmů z ne-RAII do RAII znamená víc práce, než je výsledný přínos. Navíc se nám může stát, že vytvoříme wrappery wrapperu, což zrovna na přehlednosti nepřidá.
Mnohem lépe se to řeší v C++11 díky lamba funkcím. Nejlépe to asi vysvětlí příklad.
template<typename Fn> class Finally { public: Finally(Fn fn):fn(fn) {} ~Finally() {fn();} Finally& operator=(const Finally &other); //* - pozn. na konci Finally(const Finally &other) ; //* - pozn. na konci protected: Fn fn; }; template<typename Fn> Finally<Fn> finally(Fn fn) { return Finally<Fn>(fn); } void finallyTest() { FILE *k = fopen("test.txt","w"); auto close_k = finally([k]{ fclose(k); }); fprintf(k,"Hello World\n"); //close_k se nyní vykoná, soubor se uzavře }
záznam z strace:
open("test.txt", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3 fstat(3, {st_mode=S_IFREG|0664, st_size=0, ...}) = 0 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f0cb6368000 write(3, "Hello World\n", 12) = 12 close(3) = 0 munmap(0x7f0cb6368000, 4096) = 0
Pro jistotu ještě přidám popis. Šablona Finally popisuje třídu s destruktorem, který volá funktor dodaný konstruktorem. Instance třídy si pamatuje funktor po dobu svého života. Tím je zaručeno, že se funktor vyvolá při destrukci této instance. Funkce finally() se používá ke snadné konstrukci instance. K uložení výsledku použijeme proměnnou deklarovanou jako auto, to aby nebylo potřeba zjišťovat typ výsledné konstrukce. Jako parametr funkce finally použijeme lambda funkci která se postará o uzavření zdroje, v tomto případě souboru. Schválně jsem volil Céčkovský FILE, který je klasickým ne-RAII „objektem“.
Tímto způsobem lze zajistit uzavření všech ne-RAII objektů a to bez ohledu jakým způsobem se opustí vykonávání funkce, takže to funguje jak na běžné returny uprostřed kódu, tak na výjimky.
Má to nějaké nevýhody? Jistě se nějaké najdou. Asi si všimnete, že ukazatel na soubor je v zásobníku uložen dvakrát. Naštěstí se jedná jen o ukazatel, takže to nestojí mnoho místa. Podobně se lze chovat k Windows HANDLům nebo k linuxovým FDs. Cokoliv většího ale může být problémem, zvlášť, pokud se pohybujete v prostředí malých zásobníků (nějaké jednočipy a tak), nebo generujete velké množství rekurzí.
Asi bych přesto doporučil vždy dávat přednost čistému RAII řešení, než takto vylepšovat kód. Jak jsem však napsal na začátku, tam kde by wrapování bylo na úkor čitelnosti, tam bych to použil.
Důležitá poznámka *: Takto navržený kód využívá optimalizace předávání návratové hodnoty z funkce finally(). V překladačích, které jsem testoval s tím nebyly problémy (VC2010, GCC 4.6). V zásadě jde o to, že překladač nemusí použití pro předávání výsledku z funkce finally() kopírovací konstruktor a operátor přiřazení. Tím nedojde ke spuštění destruktoru při opouštění funkce finally(). Nicméně to není podle normy. Proto jsem taky deklaroval kopírovací konstruktor a operátor přiřazení do třídy Finally, ale bez těla. Pokud je vypneme, nebo přesuneme do private sekce, bude se překladač bránit a kód nepřeloží. Takto se kód přeloží avšak je linker nebude hledat, protože se obě funkce nepoužijí. (viz Vracíme z funkce objekty) Pakliže vám linker ony dvě funkce hledá, pak je nutné třídu Finally přepsat s využitím move konstruktoru (&&) a se schopností deaktivovat destruktor po přesunu instance z funkce ven. Tím to ale trochu ztrácí na eleganci. Osobně bych to řešil podmíněným překladem podle typu překladače.
[2] O to, bych řekl, právě že jde, že destruktor close_k se volá dříve než dtor k.
Jinak v některých jazycích se ukazuje, že ani blok finally není dost, narážím teď na Javu 7 a blok try-with-resources :-) I u postupu zde v blogu naznačeném vidím hlavní problém ve výjimkách jak v hlavní smyčce, tak přímo během ~Finally, kvůli kterým by na nějaký ten úklid vůbec nemuselo dojít.
[7] Je pravdou, že výjimky zde nejsou řešeny. Asi by to stálo za úvahu. Já osobně bych výjimku povolil, tak jak výjimky v destruktorech povoluji ve všech svých projektech... za předpokladu správného používání funkce std::uncaught_exception(). Je tam ale předpoklad, že i v případě výjimky v destruktoru je objekt považován za uklizený, a výjimka jen informuje o nastalé chybě během úklidu. Potom lze výjimku "zahodit", když vznikne během úklidu kvůli jiné výjimce, a z toho důvodu se ten test rozděluje podle výsledku funkce std::uncaught_exception()
~dtor() try {
//uklid
} catch (...) {
if (std::uncaught_exception()) return; //viz norma C++, výjimky v destruktorech
}
Intenzivně se zabývám programováním zejména v jazyce C++. Vyvíjím vlastní knihovny, vzory, techniky, používám šablony, to vše proto, aby se mi usnadnil život při návrhu aplikací. Pracoval jsem jako programátor ve společnosti Seznam.cz. Nyní jsem se usadil v jednom startupu, kde vyvíjím serverové komponenty a informační systémy v C++
Přečteno 50 639×
Přečteno 23 693×
Přečteno 22 741×
Přečteno 20 723×
Přečteno 17 628×