C++ a garbage collector

Často se mluví o tom, že C++ nemá garbage collector. Jenže ona to, přísně vzato, není ani trochu pravda. Už mnoho let tvrdím, že kdo v C++ ručně uvolňuje paměť a zavírá různé zdroje, ten neumí C++. Tento článek filozofuje s otázkou, zda C++ má či nemá garbage collector.

11.8.2014 19:00 | Miloslav Ponkrác | přečteno 22907×

1. C++ a garbage collector

1.1. C++ není C s třídami

Jedna z nejhorších věcí, které může člověk dělat je, že chce předělávat jazyk či knihovnu k obrazu svému do cizí filozofie. Řada lidí posuzuje C++ jako C s třídami a přisuzují mu tytéž vlastnosti jako jazyku C. Jistě, že v C++ se dá programovat C stylem, ale je to obrovská škoda, protože C++ nabízí mnohem mnohem více. Využít možnosti C++ znamená v první řadě zapomenout C styl a nedělat věci v C++ tak, jak by je člověk dělal v C.

Pokud programujete v C++, využívejte jeho možností. C++ je velmi efektivní jazyk, umožňující mnohem efektivnější a rychlejší vývoj, než v C, aniž byste ztratili jedinou výhodu proti jazyku C. V C++ lze napsat program mnohem rychleji a efektivněji, než v C, přitom výsledek bude mnohem lépe udržovatelnější zdrojový kód oproti C a výsledná binárka bude stejně rychlá jako kdyby byla napsána v C. Abyste tohoto dosáhli, je třeba využívat možností a vlastností C++. Kdokoli má pocit, že C++ je pouze C s třídami, neříká tím o sobě nic jiného, než „já neumím C++“.

1.2. Cíle článku

V tomto článku se pokusím jednoduše zodpovědět na 3 otázky:

  1. Má C++ garbage collector pro automatické uklízení paměti?

  2. Potřebuje C++ garbage collector?

  3. Je účelné vybavit C++ garbage collectorem?

2. Má C++ garbage collector pro automatické uklízení paměti?

Správná odpověď je: Přijde na to.

C++ umožňuje vytvářet pointery, které jsou plně na zodpovědnosti programátora. A které programátor uklízí ručně a plně na své triko.

C++ umožňuje vytvářet pointery i objekty, které se automaticky dealokují a uklízejí bez starosti programátora.

Takže na otázku „Je pravdou, že C++ nemá garbage collector?“, nelze poctivě odpovědět, že C++ nemá garbage collector.

2.1. Návrhový vzor RAII

Programovací jazyk C++ pracuje na principu, že samotný C++ kompilátor velmi masivně už na úrovni jazyka C++ podporuje objektový návrhový vzor RAII (Resource Acquistion Is Initialization), nazývaný také SBRM (Scope Based Resource Management). Tento objektový návrhový vzor řídí životnost objektů, proto patří to do skupiny „creational patterns“.

Ve své podstatě je to vlastně „cé plus plus kovina“, protože je v ní cítit přesně ideologie C++. Tedy zobecňování principů až do maximální rozsahu, působení a důsledků, jaký je možný.

Dokonce právě uvědomění si základní vlastnosti programovacího jazyka C++, tedy, že se skládá v zásadě z několika jednoduchých základních principů a vlastností, které jsou dotáhnuté až do maxima, považuji za nejlepší způsob, jak se bezbolestně naučit C++. Pokud v C++ platí nějaký princip či vlastnost, pak pokud možno platí všude, na všechny typy i programátorské konsktrukce, kde je to jen trochu možné. Té vlastnosti říkám „cé plus plus kovina“, tedy obrovská vnitřní konzistence, logičnost a jednotnost celého C++ jazyka. Učit se C++ jinak, než pochopením jeho principů je velmi složité, učit se C++ pochopením jeho principů je velmi jednoduché a rychlé.

2.2. C++ dotahuje princip uklízení lokálních proměnných do absolutní dokonalosti

Člověk očekává, že ukončením podprogramu (či bloku programu) se automaticky odstraní a uklidí i lokální proměnné bez starostí programátora. Toto je ovšem ve většině programovacích jazyků nedotáhnuté, a není tomu plně tak. Vezmete-li si třeba Javu, .NET, C a mnoha dalších jazyků, nemůžete od nich očekávat to, že kompilátor zařídí také plný úklid lokálních proměnných. Uklízí se bez problémů proměnné typu integer, ale ne objekty. C++ dotáhl uklízení lokálních proměnných na plný automatický úklid a to pro jakékoli proměnné jakéhokoli typu. Tohle dotahování do konce je vlastně to, čemu říkám „cé plus plus kovina“, protože takto vzniklo skoro celé C++. Dotáhnutím principů do konce bez výjimek a nedotažeností.

Zaručený automatický úklid všech lokálních proměnných se právě nazývá RAII. Jednoduše stačí získat zdroj (paměť, soubor, grafický handle, cokoli) a uložit ho do proměnné, která ho bude vlastnit a při jejím zániku (voláním destruktoru) ho také uvolní. A tím se o to můžete přestat starat, protože zbytek zařídí C++ kompilátor, který bude uvolňovat zdroje automaticky v rámci rušení lokálních proměnných na konci oboru platnosti. A tomu se vznešeně říká RAII.

2.3. RAII versus klasický garbage collector

Ačkoli to vypadá jako nic moc, RAII je skutečně velmi dobrý garbage collector. Dokonce má další výhody:

  1. RAII uklízí nejenom paměť, ale všechny zdroje (třeba otevřené handly souborů, grafických prvků, síťové sokety, …).

  2. RAII je voláno okamžitě bez prodlení a přesně v čase, kdy zdroj přestává být potřeba.

  3. Proměnné a zdroje likviduje po skončení platnosti stejný thread jako jim dal vzniknout.

  4. Není třeba zastavovat program kvůli garbage collectoru jako v Javě, LISPu, a dalších.

Má samozřejmě i nevýhody:

  1. Je nepatrně pomalejší, protože RAII dealokuje okamžitě jedno po druhém, zatímco klasické garbage collectory dealokují naráz velké množství bloků paměti.

  2. Je třeba věnovat podpoře RAII pozornost při implementaci tříd.

2.4. C++ a uklízení sdílených proměnných

Dobrá, RAII tedy zruší již nepoužívanou paměť a zdroje lokálních proměnných. Ale co tedy uklízení paměti a zdrojů sdílených více tready nebo vranými jako návratové hodnoty?

Budete se divit, toto RAII také zvládne. Protože všechny případy automatického uklízení paměti se dají převést na uklízení lokálních proměnných. Kdy je třeba paměťový blok dealokovat? Když pointer na tento blok není obsažen v žádné proměnné, míněno v žádné lokální proměnné. Jakmile neexistuje lokální proměnná, která obsahuje adresu nějakého paměťového bloku, pak je třeba tento blok uklidit. A toto je možné pomocí RAII zařídit.

Nicméně zde je třeba použít chytré pointery z C++ standardní knihovny nebo podobnou funkcionalitu. Například std::auto_ptr nebo std::shared_ptr. Případně si napsat vlastní třídy.

Není tedy problém mít (jde o myšlenkový nástin, skutečný kód by byl odlišný):

MojeTrida * JinaTrida::getInfo()
{
  …
};

int main()
{
  JinaTrida a;
  a.getInfo();
  int x = a.getInfo()->getXParameter();
  MojeTrida * info = a.getInfo();
  int y = info->getYParameter();
  return 0;
};

A ačkoli se o to nestaráte, ve funkci main() nevznikne žádný memory leak, všechna paměť se uklidí a všechny instance třídy MojeTrida, které jsou vráceny metodou getInfo() budou automaticky uklizeny hned jak nebudou potřeba.

Jinak řečeno, v C++ opravdu je možné vracet z funkcí a metod pointery na objekty, a nechat C++ jejich automatické uklízení, aniž byste se o uklízení starali ručně.

2.5. C++ a garbage collecting

Znovu se ptám: „Má nebo nemá C++ garbage collector?“ Jak vidíte, odpovědět jde opravdu těžko. Oficiálně nemá, prakticky ale obsahuje věkerou funkčnost garbage collectoru, když o to budete stát a budete to potřebovat.

Už 10 let tvrdím, že pokud se v C++ někdo musí starat ručně o uvolňování paměti, tak neprogramuje v C++, protože ho neumí.

3. Potřebuje C++ klasický garbage collector?

V každém jazyce je dobré mít možnost garbage collectoru, jde-li to. Garbage collector je příjemná feature.

Na druhé straně garbage collector nemá jenom plusy.

3.1. Klasický garbage collector

Garbage collectorů je mnoho druhů, klasický garbage collector funguje na principu procházení všech identifikátorů v programu a uklízí pak ty bloky paměti, na které nevede žádný odkaz. A zde je kámen úrazu. Těžko bude garbage collector zjišťovat a procházet identifikátor, když program běží a identifikátory mění své hodnoty, protože programu musí stát a násilně být zastaven. A to se velmi často nehodí, přesněji, nehodí se to skoro nikdy. Pokud program dělá rychlostně kritickou činnost, pak garbage collector na chvíli zastavující program je poslední ranou do vazu. Ale i jinak, uživatel není rád, když program sekundu nereaguji na jeho kliknutí na tlačítko, protože zrovna se probral gc. To se různě eliminuje, ale je to jen zmírnění problému, nikoli jeho odstranění:

  1. Část práce se udělá za běhu programu inkrementálně.

  2. Paměťové bloky se rozdělí za několik generací, přičemž v nulté generaci jsou lokální proměnné, které se čistí okamžitě po skončení každého bloku. První a další generace pak chce už spuštění plnohodnotného gc.

  3. Opuletně se plýtvá s pamětí a gc se nespouští a nechává klidně hodiny i dny nevyčištěnou paměť v množství klidně stovek MB i několik GB.

  4. Jazyk má API, které umožňuje zakázat spuštění gc pro kritické fáze, nebo naopak doporučit spuštění gc, pokud je času dostatek.

Toto nejsou všechny problémy klasického gc. Další problém je uvolňování zdrojů. Gc se postará o uvolňování paměti, ale už ne dalších zdrojů. Programátor se o to musí starat ručně, pokud nemá docházet k plýtvání zdroji. A tak se to zase řeší nalepováky:

  1. Metody typu finalize().

  2. Metody typu dispose() v .NET.

3.2. Garbage collector typu čítač referencí

Cílem článku není kritizovat klasický garbage collector. Existují i gc, které uvolňují okamžitě a většinu výhod výše eliminují. Například čítač odkazů. Jenže ten má další nevýhody:

  1. Paměťová náročnost. Každá proměnné a každý blok má navíc 4 bajty na čítač odkazů. Dohromady je to obrovská paměťová náročnost.

  2. Problém s cyklickými odkazy, které nedokáže čítač referencí odhalit.

3.3. C++ a potřeba garbage collectoru

C++ jednoduše takové garbage collectory opravdu nepotřebuje. Jednoduše proto, že oblast používání C++ je tam, kde by vlastnosti klasického garbage collectoru či čítače referencí zničily to, co od C++ je žádáno.

Namísto toho proto C++ integrovalo RAII, které udělalo přesně to, co je třeba:

  1. Namísto procházení identifikátory a zjišťování seznamů nepotřebných bloků za běhu programu se všechno toto zařídí ve fázi kompilace programu. C++ kompilátor sám podle oboru platnosti lokálních proměnných vytvoří seznam už v době kompilace včetně automaticky generovaného kódu pro úklid paměti i zdrojů. Tím se neplýtvá cenným časem za běhu programu, stejně tak jako odpadá požadavek aby program měl přehled o všech proměnných za běhu programu.

  2. Namísto čítače referencí C++ kompilátor jednoduše detekuje místa už v době kompilace, kde má dojít k úklidu.

4. Je účelné vybavit C++ garbage collectorem?

Tato otázka už je mírně nadbytečná.

C++ má už v samotném jazyce vše potřebné pro automatické uklízení paměti a zdrojů v podpoře RAII. Jako vždy, C++ pouze nabízí, ale nevnucuje. Můžete klidně dělat program, kde budete celý životní cyklus paměti i zdrojů řídit sami, stejně jako můžete udělat program, kde uklízení bude probíhat zcela automaticky.


Článek daroval: Ing. Miloslav Ponkrác
www.ponkrac.net

Online verze článku: http://www.linuxsoft.cz/article.php?id_article=2003