Warning
This page is located in archive.

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

courses:b6b36pjc:cviceni:cviceni_8 [2017/09/25 17:15] (current)
richta created
Line 1: Line 1:
 +<​HTML>​
 +<​style>​
 +.code .kw1, .code .kw2, .code .kw4 { color: #00f; font-weight:​ bold; }
 +.code .br0, .code .kw3, .code .me1, .code .me2, .code .nu0 { color: #000; }
 +.code .co1, .code .coMULTI { color: #080; font-style: normal; }
 +.code .co2 { color: #888; font-style: normal; }
 +.code .st0 { color: #a31515; }
 +</​style>​
 +</​HTML>​
 +
 +===== Dědičnost =====
 +
 +Cílem dnešního cvičení je naučit se rozpoznat různá nebezpečí,​ která se skrývají v dědění a objektově orientovaném polymorfismu v C%%++%%. Začněme následujícím kódem:
 +
 +<code cpp>
 +#include <​iostream>​
 +
 +class Writable {
 +public:
 +};
 +
 +class Foo : public Writable {
 +public:
 +};
 +
 +class Bar : public Writable {
 +public:
 +};
 +
 +int main() {
 +    Writable* writ = new Writable;
 +    Writable* foo = new Foo;
 +    Writable* bar = new Bar;
 +    delete writ;
 +    delete foo;
 +    delete bar;
 +}
 +</​code>​
 +
 +Třída ''​Writable''​ má být společným předkem všech tříd, které lze vypsat na standardní výstup metodou ''​write''​. Teď bude naším cílem metodu ''​write''​ naimplementovat. Všimněte si, že můžeme na potomky třídy ''​Writable''​ odkazovat ukazatelem typu ''​Writable*''​. Stejně tak můžeme použít chytré ukazatele, ale zatím nebudeme ''​unique_ptr''​ používat, abychom mohli lépe uvažovat o tzv. virtuálním destruktoru (viz. dále).
 +
 +První pokus o implementaci metody ''​write''​ by mohl vypadat takto:
 +
 +<code cpp>
 +class Writable {
 +public:
 +    void write(std::​ostream&​ out) const { out << "​Writable\n";​ }
 +};
 +
 +class Foo : public Writable {
 +public:
 +    void write(std::​ostream&​ out) const { out << "​Foo\n";​ }
 +};
 +
 +class Bar : public Writable {
 +public:
 +    void write(std::​ostream&​ out) const { out << "​Bar\n";​ }
 +};
 +</​code>​
 +
 +Naším záměrem je změnit funkcionalitu metody ''​write''​ podle toho, jaký je přesný typ objektu. Přidejme volání metody ''​write''​ pro každý z ukazatelů ''​writ'',​ ''​foo''​ a ''​bar'':​
 +
 +<code cpp>
 +    writ->​write(std::​cout);​ // Writable
 +    foo->​write(std::​cout); ​ // Foo
 +    bar->​write(std::​cout); ​ // Bar
 +</​code>​
 +
 +Zde nárážíme na první nepříjemné překvapení,​ protože dostaneme následující výstup:
 +
 +<code cpp>
 +Writable
 +Writable
 +Writable
 +</​code>​
 +
 +
 +===== Virtuální dědičnost =====
 +
 +Kde se stala chyba? Problém je v tom, že jsme neoznačili metodu ''​write''​ jako //​virtuální//​. Pouze volání virtuálních metod způsobí vyhledávání správné implementace pro danou metodu. Takové volání nazýváme //​dynamické//​.
 +
 +Podmínky pro dynamické volání metody ''​fun''​ třídy ''​T''​ jsou:
 +  * ''​fun''​ je virtuální metoda ve třídě ''​T''​ a
 +  * volání je ve tvaru ''​obj%%->​%%fun(...)'',​ kde ''​obj''​ je ukazatel na ''​T'',​ nebo
 +  * volání je ve tvaru ''​obj.fun(...)'',​ kde ''​obj''​ je reference na ''​T''​.
 +
 +Všimněte si, že dynamického volání nelze dosáhnout bez ukazatele nebo reference. Když máme přímo hodnotu typu ''​T'',​ nemůže se jednat o potomka, který dědí z ''​T'';​ musí to být právě ''​T''​. To se projevuje neintuitivním chováním při kopírování z předka do potomka (tzv. [[http://​stackoverflow.com/​questions/​274626/​what-is-object-slicing|slicing problem]]).
 +
 +Virtuální metodu zavedeme klíčovým slovem ''​virtual'':​
 +
 +<code cpp>
 +class Writable {
 +public:
 +    virtual void write(std::​ostream&​ out) const { out << "​Writable\n";​ }
 +};
 +
 +class Foo : public Writable {
 +public:
 +    virtual void write(std::​ostream&​ out) const { out << "​Foo\n";​ }
 +};
 +
 +class Bar : public Writable {
 +public:
 +    virtual void write(std::​ostream&​ out) const { out << "​Bar\n";​ }
 +};
 +</​code>​
 +
 +Přísně vzato bychom nemuseli psát ''​virtual''​ u dědiců ''​Foo''​ a ''​Bar'',​ ale je to více než slušné. Informujeme tím čtenáře kódu, že dochází k dynamickému volání.
 +
 +Je důležité,​ aby název a parametry dané virtuální metody byly zcela shodné ve všech dědících metodách. Pokud bychom například pojmenovali omylem jednu z metod ''​wirte''​ místo ''​write'',​ nedošlo by k dynamickému volání. Stejně tak přidání druhého (ač nepovinného) parametru by zabránilo tomu, aby se zavolala námi zamýšlená definice metody. Těmto druhům chyb zabrání klíčové slovo ''​override'',​ které oznamuje náš záměr nahradit virtuální metodu předka:
 +
 +<code cpp>
 +class Writable {
 +public:
 +    virtual void write(std::​ostream&​ out) const { out << "​Writable\n";​ }
 +};
 +
 +class Foo : public Writable {
 +public:
 +    virtual void write(std::​ostream&​ out) const override { out << "​Foo\n";​ }
 +};
 +
 +class Bar : public Writable {
 +public:
 +    virtual void write(std::​ostream&​ out) const override { out << "​Bar\n";​ }
 +};
 +</​code>​
 +
 +Nyní pokud nebude v předkovi odpovídající deklarace, program se nezkompiluje. V tuto chvíli je rozumné v dědicích ''​Foo''​ a ''​Bar''​ vypustit klíčové slovo ''​virtual'',​ protože ''​override''​ samo o sobě vyžaduje, aby metoda byla virtuální.
 +
 +===== Zrádný destruktor =====
 +
 +Pojďme zkoumat životní cyklus objektů uvnitř našich tříd. Nápomocná nám bude třída, která oznamuje svůj vznik a zánik výpisem na standardní výstup.
 +
 +<code cpp>
 +struct LisNaJesterkyAHady {
 +    LisNaJesterkyAHady() { std::cout << "Lis vytvoren!\n";​ }
 +    ~LisNaJesterkyAHady() { std::cout << "Lis znicen!\n";​ }
 +};
 +</​code>​
 +
 +Umístěte objekt typu ''​LisNaJesterkyAHady''​ na různá místa v programu a pozorujte výpisy na standardní výstup:
 +  * Vytvořte objekt jako lokální proměnnou ve funkci ''​main''​.
 +  * Umístěte objekt do třídy ''​Writable''​.
 +  * Umístěte objekt do tříd ''​Foo''​ a ''​Bar''​.
 +
 +Vidíme, že ve třetím případě dojde k úniku paměti -- ani jeden lis nebyl smazán. Nachytali jsme se totiž na další neintuitivní chování dědičnosti v C%%++%%: stejně jako ostatní metody, i destruktor musí být označen jako virtuální,​ aby se zavolal dynamicky. Bohužel, automaticky generovaný destruktor není virtuální,​ pokud není virtuální destruktor předka. Je trestuhodné,​ že řada rozšířených kompilátorů nás nevaruje, i když máme ve třídě jiné virtuální metody.
 +
 +Chybu opravíme takto:
 +
 +<code cpp>
 +class Writable {
 +public:
 +    virtual ~Writable() = default;
 +    ...
 +};
 +
 +class Foo : public Writable {
 +public:
 +    virtual ~Foo() override = default;
 +    ...
 +};
 +
 +class Bar : public Writable {
 +public:
 +    virtual ~Bar() override = default;
 +    ...
 +};
 +</​code>​
 +
 +Jak bylo zmíněno výše, stačilo by deklarovat destruktor ''​Writable''​ jako virtuální,​ a vše by již fungovalo. Klíčové slovo ''​override''​ u destruktorů ve ''​Foo''​ a ''​Bar''​ má ale výhodu, že virtualní destruktor ve ''​Writable''​ vynutí; kdyby tam chyběl, program se nezkompiluje. Protože nepotřebujeme v žádném z těchto destruktorů provést nic speciálního,​ použijeme kompilátorem generované tělo pomocí ''​= default''​.
 +
 +  * Vyměňte všechny ukazatele v kódu za chytré ukazatele ''​std::​unique_ptr<​Writable>''​.
 +  * Vytvořte třídu ''​String'',​ která dědí ''​Writable''​ a v metodě ''​write''​ vypíše řetězec, který získá konstruktorem.
 +  * Podobně vytvořte třídu ''​Number'',​ která umí vypsat číslo typu ''​double'',​ které získá konstruktorem.
 +  * Odstraňte třídy ''​Foo''​ a ''​Bar''​.
 +
 +{{:​courses:​a7b36pjc:​cviceni:​cviceni_8_mid.zip|Řešení}}
 +
 +===== Hierarchie pomocí dědičnosti =====
 +
 +Teď, když máme virtuální destruktory,​ nic nám nebrání vlastnit objekty pomocí ukazatele na předka. Pokud bychom chtěli uložit různé potomky třídy ''​Writable''​ do kolekce, poslouží nám vektor ukazatelů, volitelně chytrých:
 +
 +<code cpp>
 +std::​vector<​Writable*>​ writables;
 +std::​vector<​std::​unique_ptr<​Writable>>​ writables;
 +</​code>​
 +
 +Co naopak //nebude// fungovat je uložit si přímo hodnoty daného typu, protože stále platí, že hodnota typu ''​Writable''​ musí být právě ''​Writable'',​ nikoli některý z jeho potomků.
 +
 +<code cpp>
 +std::​vector<​Writable>​ writables; // takto ne
 +</​code>​
 +
 +Vytvořme novou třídu dědící z ''​Writable'',​ která sama vlastní nějaký počet vypisovatelných objektů.
 +
 +  * Vytvořte novou třídu ''​Group''​ dědící z ''​Writable''​.
 +  * Do ''​Group''​ přidejte jako data objekt typu ''​std::​vector<​std::​unique_ptr<​Writable>>''​.
 +  * Přidejte do ''​Group''​ metodu ''​push_back'',​ která má parametr typu ''​Writable*''​. Voláním metody ''​Group''​ převezme vlastnictví odkazovaného objektu.
 +  * Přidejte do třídy ''​Group''​ tři řetězce ''​m_prefix'',​ ''​m_separator'',​ ''​m_suffix''​.
 +  * Nahraďte ve třídě ''​Group''​ poděděnou metodu ''​write''​. V ní vypište všechny vlastněné objekty oddělené řetězcem ''​m_separator'',​ navíc celý výpis uvěďte řetězcem ''​m_prefix''​ a zakončete řetězcem ''​m_suffix''​.
 +  * Vytvořte ve třídě ''​Group''​ konstruktor,​ který jako parametry dostává tři řetězce.
 +  * Ve třídách ''​Number''​ a ''​String''​ nevypisujte konce řádků za číslem/​řetězcem.
 +
 +Vyzkoušejte kód na následující funkci main:
 +
 +<code cpp>
 +int main() {
 +    Group g("​(",​ ", ", "​)"​);​
 +    g.push_back(new Number(123));​
 +    g.push_back(new Number(456));​
 +    g.push_back(new String("​abc"​));​
 +    g.push_back(new String("​def"​));​
 +    ​
 +    std::​unique_ptr<​Group>​ gp(new Group("​[",​ " | ", "​]"​));​
 +    gp->​push_back(new Number(789));​
 +    gp->​push_back(new String("​ghi"​));​
 +    g.push_back(gp.release());​
 +
 +    g.write(std::​cout);​ // (123, 456, abc, def, [789 | ghi])
 +    std::cout << '​\n';​
 +}
 +</​code>​
 +
 +===== Dynamické volání v nezávislé funkci =====
 +
 +Bylo by rozumné, aby nám fungoval operátor ''​%%<<​%%''​ pro výstup do proudu pro všechny dědice třídy ''​Writable''​. To uděláme snadno tak, že vezmeme referenci na ''​Writable'':​
 +
 +<code cpp>
 +std::​ostream&​ operator<<​(std::​ostream&​ out, const Writable&​ writable) {
 +    writable.write(out);​
 +    return out;
 +}
 +</​code>​
 +
 +Teď můžeme psát:
 +
 +<code cpp>
 +std::cout << g << '​\n';​
 +</​code>​
 +
 +{{:​courses:​a7b36pjc:​cviceni:​cviceni_8_final.zip|Řešení}}
 +
 +===== Další nebezpečenství dědění =====
 +
 +  * **Kopírování nefunguje.** Nemůžeme udělat něco jako virtuální kopírující konstruktor a spol. Pokud chceme zkopírovat objekt neznámého typu, musíme vytvořit vlastní metodu, která je virtuální (''​copy'',​ ''​clone'',​ nebo něco podobného).
 +  * **Dynamické volání nefunguje v konstruktoru.** V konstruktoru nelze zavolat variantu metody z dědící třídy, protože předek existuje dříve, než potomek.
  
courses/b6b36pjc/cviceni/cviceni_8.txt · Last modified: 2017/09/25 17:15 by richta