{{page>courses:b6b36pjc:styles#common&noheader&nofooter}} {{page>courses:b6b36pjc:styles#cviceni&noheader&nofooter}} ===== Cvičení 9: Kopírování polymorfních objektů ===== Minule jsme viděli, jak nám objektově orientované programování umožňuje vytvářet hierarchie heterogenních objektů. Pod ukazatelem či referencí na ''Writable'' se mohly skrývat objekty různých odvozených tříd -- ''String'', ''Number'' a ''Group''. Ačkoliv jsme měli pouhý odkaz na předka, virtuální destruktor nám umožnil všechny objekty správně smazat. Virtuální metoda ''write'' nám umožnila vypsat všechny objekty pomocí jednotného rozhraní. std::unique_ptr w(new String("abc")); w->write(std::cout); // zavolá se správné write // konec života w: zavolá se správný destruktor Dnes se naučíme kopírovat objekty pomocí odkazu na předka. Jak už jsme zmínili minule, kopírování je v takovéto situaci problematické. Naštěstí existuje standardní a osvědčený způsob, jak se s ním vypořádat. {{:courses:b6b36pjc:cviceni:cviceni_9_init.zip|Výchozí kód}} Než budeme vytvářet kopie, začneme přesuny, které bývají jednodušší a umožní nám získat hodnotu zpět z ukazatele. std::unique_ptr g("(", ", ", ")"); g.push_back(new Number(456)); g.push_back(new String("def")); Group g2 = std::move(*g); * Naimplementujte přesunující konstruktor (a přesunující přiřazení) třídy ''Group''. Následující kód musí fungovat: int main() { Group g("[", " | ", "]"); g.push_back(std::make_unique(123)); g.push_back(std::make_unique("abc")); Group g2("(", ", ", ")"); g2.push_back(std::make_unique(456)); g2.push_back(std::make_unique("def")); g.push_back(std::make_unique(std::move(g2))); std::cout << g << '\n'; } Poněkud složitější je vytvořit //kopírující// konstruktor třídy ''Group''. Pojďmě si to vyzkoušet. * Naimplementujte kopírující konstruktor (a kopírující přiřazení) třídy ''Group''. Následující kód musí fungovat: int main() { Group g("[", " | ", "]"); g.push_back(std::make_unique(123)); g.push_back(std::make_unique("abc")); Group g2("(", ", ", ")"); g2.push_back(std::make_unique(456)); g2.push_back(std::make_unique("def")); g.push_back(std::make_unique(g2)); g2.push_back(std::make_unique("ghi")); std::cout << g << '\n'; // [123 | abc | (456, def)] std::cout << g2 << '\n'; // (456, def, ghi) } Během implementace jsme zjistili, že potřebujeme vytvořit kopii objektu, na který odkazujeme jako na předka (pomocí ''Writable*'' nebo ''Writable&''). To je ale problém, protože není jasné, který konstruktor zavolat. Řešením byla virtuální metoda ''clone'', která má za úkol provést kopii správným způsobem. Metoda ''clone'' se ale dá trochu vylepšit, využitím kovariantních návratových typů. {{:courses:b6b36pjc:cviceni:cviceni_9_copy.zip|Řešení}} ==== Kovariantní návratové typy ==== C%%++%% umožňuje používat tzv. kovariantní návratové typy. To znamená, že nahrazující metoda dědící třídy může mít jiný návratový typ, než nahrazená metoda rodiče. Návratové typy se ale nemohou měnit libovolně, musí se jednat o ukazatel a musí se jednat o ukazatel na třídu která dědí z třídy, na kterou vracela ukazatel metoda rodiče. V našem případě to můžeme využít takto: class Writable { public: ... virtual Writable* clone() const = 0; }; class String : public Writable { public: ... virtual String* clone() const { return new String(*this); }; ... }; Takto napsaná metoda ''clone'' nám umožňuje získat ukazatel stejného typu, jako staticky víme, že máme a nemusíme tedy provádět přetypování za běhu programu. {{:courses:b6b36pjc:cviceni:cviceni_9_covariant_clone.zip|Řešení používající kovariantní návratový typ}} Kovariantní návratové typy se mohou vztahovat i na jiné hierarchie tříd: class FTPServer { public: virtual FTPClient* acceptConnection(); }; class SFTPServer : public FTPServer { public: virtual SFTPClient* acceptConnection(); }; Pokud ale budeme chtít vracet chytrý ukazatel, aby se uživatel třídy nemusel starat o smazání objektu, narazíme na problém. Jak jsme říkali, kovariantní návratové typy se týkají ukazatelů, ale ''std::unique_ptr'' z hlediska jazyka není ukazatel, ale obyčejný objekt. To znamená, že pokud chceme aby ''clone'' vracelo chytrý ukazatel, ''clone'' musí vracet ''std::unique_ptr''. class Writable { public: ... virtual std::unique_ptr clone() const = 0; }; class String : public Writable { public: ... virtual std::unique_ptr clone() const { return std::make_unique(*this); }; ... }; Jak je ale vidět v ukázce, rvalue reference unique_ptr se kovariantně chovají. {{:courses:b6b36pjc:cviceni:cviceni_9_unique_clone.zip|Řešení používající std::unique_ptr}} ===== Výjimky ===== Jak už víte z přednášky, obsluha chyb v C%%++%% může mít tři podoby: speciální návratové hodnoty, chybové příznaky a výjimky. Dnes si ukážeme základní práci s výjimkami: házení a obsluhu. ==== Standardní výjimky ==== Rozšiřme předchozí příklad o házení výjimek. Objekty typu ''Number'' jsou totiž ošklivé a rozmazlené a některá čísla jim prostě nechutnají. Vyjímku vyhodíme tak, že použijeme klíčové slovo ''throw'': #include ... throw std::invalid_argument("I don't like that number!"); * Vyhoďte výjimku v konstruktoru třídy ''Number'', pokud pro argument ''n'' platí ''std::cos(n) < 0''. Vyhodili jsme typ ''std::invalid_argument''. Existuje řada takovýchto standardních výjimek, např. ''std::runtime_error'', ''std::bad_alloc'', a další. Protože jsou všechny součástí hierarchie objektů se společným předkem ''std::exception'', následující je zachytí všechny: try { // zde může nastat výjimka } catch (const std::exception& e) { std::cerr << "An exception was thrown:\n"; std::cerr << e.what() << '\n'; } // volitelně: catch (...) { std::cerr << "A totally unknown exception was thrown!\n"; } Pokud zadáme tři tečky místo typu výjimky, ''catch'' blok se provede pro všechny druhy vyjímek, které nebyly obslouženy žádným předchozím ''catch'' blokem. Všimněte si, že výjimku přijímáme konstantní referencí (konec konců, ze ''std::exception'' se dědí). * Zachyťte výjimku v funkci ''main''. Teď bychom měli při spuštění programu vidět následující výstup: An exception was thrown: I don't like that number! Je s výhodou, že náš program nespadl, na druhou stranu teď vůbec nevíme, kde přesně došlo k chybě. Způsobilo výjimku číslo ''123'', nebo až ''456''? Není jasné, který kus kódu máme spravit, aby byl konstruktor ''Number'' spokojený. ==== Vlastní výjimky ==== Protože standardní knihovna obsahuje pouze omezené množství typů výjimek, budeme občas chtít definovat výjimky vlastní. Takto může uživatel našeho kódu lépe vymezit, jaké chyby chce ošetřit. Vytvoříme si tedy pro Number novou výjimku, ''IDontLikeThatNumberException''. * Vytvořte novou třídu ''IDontLikeThatNumberException'', která dědí ze ''std::exception''. V jejím konstruktoru získejte číslo, které chcete vypsat na výstup při volání metody ''what''. * Také si v této třídě uložte celý řetězec, který chcete vypsat v případě volání metody ''what''. Řetězec je nejlepší vytvořit a uložit hned v konstruktoru. * Nahraďte metodu ''what''. Teď bychom měli mít možnost chytat pouze výjimku, která vznikne uvnitř třídy ''Number'': catch (const IDontLikeThatNumberException& e) { std::cerr << "IDontLikeThatNumberException was thrown:\n"; std::cerr << e.what() << '\n'; } {{:courses:b6b36pjc:cviceni:cviceni_9_final.zip|Řešení}} ==== Co je ještě dobré vědět o výjimkách ==== * Pokud se nezdaří dynamická alokace paměti pomocí operátoru ''new'', vyhodí se vyjímka ''std::bad_alloc''. * Vytváření objektu typu ''std::string'' obvykle obnáší dynamickou alokaci paměti. Proto není vždy dobrý nápad používat ho ve výjimce -- například tehdy, když výjimkou oznamujeme nedostatek paměti((Z tohoto důvodu ''std::bad_alloc'' nemá konstruktor beroucí řetězec oznamující důvod chyby)). * Nikdy neházejte výjimky v destruktorech! Jak víme, letící výjimka volá destruktory. Co se má stát, když vyletí //druhá// výjimka? Standard určuje, že v tom případě se má program okamžitě ukončit.