Jste zde

Ještě jednou a lépe: správa prostředků v C++

Dnes se podíváme na velmi užitečný koncept zvaný RAII, který usnadňuje správu prostředků a vede ke korektnějšímu kódu bez zbytečných duplicit.

Původní kód

Představte si následující funkci.

void foo() {
    Resource *r = getResource(); // Vrací dynamicky alokovaný objekt.
 
    // ...
 
        delete r;
        return;
 
    // ...
 
        delete r;
        return;
 
    // ...
 
    delete r;
    return;
}

Získáme si v ní dynamicky alokovaný objekt r typu Resource, se kterým pracujeme. Před návratem z funkce je poté potřeba objekt korektně dealokovat. Vlivem větvení je však míst návratu z funkce celá řada a tak se dealokace vyskytuje vícekrát.

Proč takto ne?

  • Složitá správa. Je potřeba pamatovat na to, aby před každým návratem z funkce došlo k dealokování r. Pokud bychom v budoucnu funkci rozšířili o další návrat, tak se na to velmi snadno zapomene.
  • Přístup není bezpečný z hlediska výjimek. Neposkytuje ani základní úroveň tzv. exception safety. Pokud totiž dojde ve funkci k vyhození výjimky, tak se dealokace r neprovede, protože se zbylý kód ve funkci od místa vyhození výjimky nevykoná.
  • Duplicitní kód. V neposlední řadě funkce obsahuje duplicitní kód (ono delete r). Pokud dojde ke změně způsobu správy onoho prostředku, tak je potřeba zaručit, že budou změněna všechna místa, kde se delete r provádí. Pokud bychom ve funkci pracovali s více takovými objekty, tak množství duplikovaného kódu ještě více naroste.
  • Použití komentáře. Abychom zdůvodnili, proč na r voláme delete, tak u volání getResource() napíšeme komentář. Pokud bychom se mohli použití komentáře vyhnout, aniž by utrpěla čitelnost kódu, tak by to bylo plus (komentáře mají své nevýhody, např. že oproti kódu snadno zastarávají).

Jak to udělat lépe?

Využijeme koncept zvaný RAII, což je zkratka z anglického Resource Acquisition Is Initialization. Funguje to tak, že spravovaný zdroj umístíme při inicializaci do lokálního objektu, který se ve svém destruktoru automaticky postará o jeho korektní uvolnění.

V našem případě, kdy se jedná o práci s dynamicky alokovanou pamětí využijeme std::unique_ptr. Upravený kód bude pak vypadat takto:

void foo() {
    std::unique_ptr<Resource> r(getResource());
 
    // ...
 
        return;
 
    // ...
 
        return;
 
    // ...
 
    return;
}

Zaručíme si tak automatické uvolnění paměti, kód je nyní bezpečný z hlediska výjimek (destruktor od r se zavolá i v případě, kdy dojde uvnitř foo() k vyhození výjimky) a zmizel duplicitní kód i komentář.

Dále, pokud funkce getResource() pochází od nás, tak bychom ji měli taktéž upravit. Konkrétně bychom měli změnit

Resource *getResource();

na

std::unique_ptr<Resource> getResource();

Tím explicitně říkáme, že volající getResource() se stává vlastníkem onoho ukazatele. To nebylo dříve ze signatury funkce zřejmé (maximálně z komentáře, ale ten se snadno opomine).

Poznámky

  • RAII se netýká jen správy paměti. Lze jej využít v různorodých případech, např. pro automatické zavírání souborů, automatické uvolnění zámků při paralelním programování, automatické ukončení spojení (sockety) atd.
  • Standardní C++ knihovna RAII hojně využívá, např. chytré ukazatele (angl. smart pointers), streamy (automatické uzavření souboru) a další.
  • Pokud byste potřebovali RAII pro něco, pro co není ve standardní knihovně podpora, tak není vůbec žádný problém si podporu přidat. Stačí si vytvořit třídu, která v konstruktoru zahájí správu prostředku a v destruktoru prostředek uvolní.
  • Automatická správa paměti v podobě gargabe collectoru není univerzálním řešením. Funguje totiž pouze pro paměť, kdežto prostředky mohou být různorodé (viz první bod). Navíc typicky nemáte 100% kontrolu nad tím, kdy se garbage collector spustí. U RAII to víte: destruktor se vykoná vždy při opuštění bloku (i při vyhozené výjimce).
  • RAII lze v některých jazycích emulovat pomocí try-finally. Typickým příkladem je Java před verzí 7. C++ tuto konstrukci nemá (pouze try-catch), což ale tolik nevadí, protože má RAII, jež může přinést ve výsledku lepší kód (kratší a čitelnější). Pokud byste ale chtěli, tak od C++11 si můžete try-finally emulovat přes RAII a lambda funkce.
  • V Pythonu slouží ke správě prostředků příkaz with. V Javě lze od verze 7 použít příkaz try-with-resources.
  • Pokud vám onen název RAII příliš nesedí, tak nejste sami. Možná by pro tento koncept šlo vymyslet lepší pojmenování, ale co se dá dělat; tento termín se již vžil.

Aktualizace 19.10.2015: Doporučuji se podívat na tuto skvělou přednášku z CppConu, kde Andrei Alexandrescu využívá RAII pro zkvalitnění a zčitelnění kódu.

Přidat komentář