Hlavní navigace

Náhrada chybějícího finally v C++11

6. 9. 2012 8:11 (aktualizováno) | Ondřej Novák

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.