===== 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: #include 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; } 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: 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"; } }; 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'': writ->write(std::cout); // Writable foo->write(std::cout); // Foo bar->write(std::cout); // Bar Zde nárážíme na první nepříjemné překvapení, protože dostaneme následující výstup: Writable Writable Writable ===== 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'': 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"; } }; 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: 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"; } }; 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. struct LisNaJesterkyAHady { LisNaJesterkyAHady() { std::cout << "Lis vytvoren!\n"; } ~LisNaJesterkyAHady() { std::cout << "Lis znicen!\n"; } }; 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: class Writable { public: virtual ~Writable() = default; ... }; class Foo : public Writable { public: virtual ~Foo() override = default; ... }; class Bar : public Writable { public: virtual ~Bar() override = default; ... }; 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''. * 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: std::vector writables; std::vector> writables; 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ů. std::vector writables; // takto ne 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>''. * 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: 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 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'; } ===== 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'': std::ostream& operator<<(std::ostream& out, const Writable& writable) { writable.write(out); return out; } Teď můžeme psát: std::cout << g << '\n'; {{: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.