{{page>courses:b6b36pjc:styles#common&noheader&nofooter}} {{page>courses:b6b36pjc:styles#cviceni&noheader&nofooter}} ===== Cvičení 6: Kopie a přesuny ===== ==== Typový systém ==== Zopakujme si, jaké typy proměnných a parametrů se mohou vyskytovat v jazyce C%%++%%. ^ Typ ^ Název ^ Příklad přiřazení ^ Příklad parametru funkce ^ Může ''b'' být \\ ''const T''? ^ Změna ''a'' \\ změní ''b''? ^ Změna ''*a'' \\ změní ''b''? ^ | ''T'' | hodnota | ''T a = b;'' | ''void f(T a); f(b);'' | ano | | | | ''T*'' | ukazatel | ''T* a = &b;'' | ''void f(T* a); f(&b);'' | | | ano | | ''const T*'' | ukazatel na konst. | ''const T* a = &b;'' | ''void f(const T* a); f(&b);'' | ano | | | | ''T&'' | reference | ''T& a = b;'' | ''void f(T& a); f(b);'' | | ano | | | ''const T&'' | konst. reference | ''const T& a = b;'' | ''void f(const T& a); f(b);'' | ano | | | ==== Kopírování ==== Kopírování je nejběžnější operace v C%%++%%. Ke kopírování dochází vždy, když použijeme operátor přiřazení ''='', ale také v jiných situacích; třeba tehdy, když vytváříme nový objekt z jiného. Pojďme se nejdříve zaměřit na případ, že vytváříme nový objekt z jiného, již existujícího objektu. Vzniklý objekt nazýváme **kopie**. void foo(int c) { // ... } int main() { int i = 13; int a = i; // a je kopie i int b(i); // b je kopie i foo(i); // c je kopie i } Funkce, která zajíšťuje tvorbu kopií, se nazývá **kopírující konstruktor**. Podobně jako základní konstruktor i kopírující konstruktor je automaticky vytvořen kompilátorem, pokud nenapíšeme vlastní. Díky tomu je možné vytvářet kopie vlastních typů, aniž bychom kopírující konstruktor museli psát: struct MyStruct { int a; double b; }; void foo(MyStruct c) { // ... } int main() { MyStruct i = { 11, 2.9 }; MyStruct a = i; // a je kopie i MyStruct b(i); // b je kopie i foo(i); // c je kopie i } U některých tříd je kompilátorem generovaný kopírující konstruktor nežádoucí. * Stáhněte si soubor: {{:courses:b6b36pjc:cviceni:cviceni_6_copy_fail.zip|Zip}} [[http://coliru.stacked-crooked.com/a/14e2176a8faf45da|Coliru]] * Zkompilujte program a spusťte ho. Co se stane? * Pokuste se chování programu vysvětlit. * Napište vlastní kopírující konstruktor pro třídu ''vector''. Kopírující konstruktor bere jako parametr ''const vector&''. Někdy bychom rádi, aby se naše objekty kopírovat nedaly. K tomu slouží ''= delete'' v deklaraci kopírujícího konstruktoru: class vector { public: ... vector(const vector& rhs) = delete; } Příkladem objektu ze standardní knihovny, který nelze kopírovat, je ''std::unique_ptr''. Pokud máme ''unique_ptr'' v naší třídě, kopírující konstruktor se automaticky nevytvoří. * Stáhněte si ''vector'' s ''unique_ptr'' z minulého cvičení: {{:courses:b6b36pjc:cviceni:cviceni_5_unique_ptr.zip|}} * Pokuste se vytvořit kopii ''vector''u, třeba pomocí ''vector v2 = v;''. Co se stane? * Vytvořte kopírující konstruktor pro třídu ''vector''. ==== Přesun ==== Přesun je podobný kopii, ale umožňuje změnit objekt, ze kterého kopírujeme. Přesun nastává v těchto situacích: * Při návratu z funkce pomocí ''return''. * Pokud si to vyžádáme pomocí ''std::move()''. Příklady přesunů: #include #include void print_vector(const std::vector& v) { for (auto& item : v) std::cout << item << ' '; std::cout << '\n'; } std::vector get() { std::vector v = { 1, 23, 4, 1 }; return v; } int main() { std::vector v1 = get(); // v přesunut do v1 std::vector v2 = std::move(v1); // v1 přesunut do v2 std::cout << "v1: "; print_vector(v1); // v1: std::cout << "v2: "; print_vector(v2); // v2: 1 23 4 1 } Účelem přesunu je zefektivnit některé operace. V předchozím kódu bychom například mohli vědět, že ''v1'' už nebudeme potřebovat, proto jsme ho do ''v2'' přesunuli. Pokud vracíme hodnotu ve funkci, tak není pochyb, že původní objekt dál už nepotřebujeme. Funkce ''std::move'' má speciální návratový typ zvaný //rvalue reference//. Rvalue reference se píše ''T&&'' a značí referenci na objekt, který má vzápětí zaniknout. Díky tomu můžeme odlišit další druh konstruktoru, tzv. **přesunující konstruktor**. * Přidejte do třídy ''vector'' přesunující konstruktor, jeho parametr bude typu ''vector&&''. V těle konstruktoru si přivlastněte pole původního objektu a původní objekt nastavte na prázdný. * Přidejte do všech konstruktorů a destruktoru kontrolní výpisy, které prozradí, kdy se která z těchto funkcí volá. * Vyzkoušejte upravenou třídu pomocí této funkce ''main'': int main() { vector v1; v1.push_back(1.23); v1.push_back(2.34); std::cout << '\n'; vector v2 = v1; std::cout << '\n'; vector v3 = std::move(v2); std::cout << '\n'; std::cout << "v1: "; print_vector(v1); std::cout << "v2: "; print_vector(v2); std::cout << "v3: "; print_vector(v3); } Pokud doplníme typový systém o rvalue reference, získáme takovouto tabulku: ^ Typ ^ Název ^ Příklad přiřazení ^ Příklad parametru funkce ^ Může ''b'' být \\ ''const T''? ^ Změna ''a'' \\ změní ''b''? ^ Změna ''*a'' \\ změní ''b''? ^ | ''T'' | hodnota | ''T a = b;'' | ''void f(T a); f(b);'' | ano | | | | ''T*'' | ukazatel | ''T* a = &b;'' | ''void f(T* a); f(&b);'' | | | ano | | ''const T*'' | ukazatel na konst. | ''const T* a = &b;'' | ''void f(const T* a); f(&b);'' | ano | | | | ''T&'' | reference | ''T& a = b;'' | ''void f(T& a); f(b);'' | | ano | | | ''const T&'' | konst. reference | ''const T& a = b;'' | ''void f(const T& a); f(b);'' | ano | | | | ''T&&'' | rvalue reference | ''T&& a = std::move(b);'' | ''void f(T&& a); f(std::move(b));'' | | ano | | ==== Přiřazení ==== Našemu ''vectoru'' stále ještě chybí jedna důležitá schopnost, neumí totiž správně zkopírovat hodnotu do //již existujícího// objektu. Jinými slovy, tento kód stále skončí katastrofou: int main() { vector v1; vector v2; v1 = v2; } Stali jsme se obětí automaticky generovaného **kopírujícího přiřazení**. To se zavolá v případě, že použijeme operátor ''='' na již existující objekt. I tuto speciální funkci si budeme muset naimplementovat sami. Uděláme to tak, že přetížíme operátor ''=''. * Přidejte do třídy ''vector'' kopírující přiřazení. Deklarace kopírujícího přiřazení bude vypadat takto: ''vector& operator=(const vector& rhs)''. Uvnitř funkce zkopírujte obsah původního objektu ''rhs'' do tohoto objektu. Přiřazení bude vracet referenci na tento objekt, proto by poslední řádek měl být ''return *this;''. * Co se stane, když ''rhs'' je tentýž objekt, jako ''*this''? Pokud se stane něco špatného, funkci opravte. * Pro úplnost přidejte také **přesunující přiřazení**. Deklarace přesunujícího přiřazení bude vypadat takto: ''vector& operator=(vector&&)''. * Vyzkoušejte upravenou třídu na této funkci ''main'': int main() { vector v1; v1.push_back(1.23); v1.push_back(2.34); vector v2; v2 = v1; vector v3; v3 = std::move(v2); std::cout << "v1: "; print_vector(v1); std::cout << "v2: "; print_vector(v2); std::cout << "v3: "; print_vector(v3); } {{:courses:b6b36pjc:cviceni:cviceni_6_move.zip|Řešení}} ==== Využití swapu ==== Implementace některých speciálních členských funkcí, jmenovitě kopírujícího přiřazení, přesunujícího konstruktoru a přesunujícího přiřazení, se dá velmi zjednodušit za pomoci metody ''swap'', jejíž úkol je prohodit veškerý obsah dvěma objektům typu ''vector''. * Implementujte ve třídě ''vector'' metodu ''swap'', která vymění obsah dvou vektorů. * Deklarace této metody je ''void swap(vector& rhs)'' a po jejím skončení by měl být obsah obou vektorů prohozen bez toho, aby se přesouvaly jednotlivé prvky. * Ke zjednodušenní prohazování hodnot v proměnných můžete použít knihovní funkci ''std::swap'' z hlavičky ''''. * Zjednodušte kopírující přiřazení, přesunující konstruktor a přesunující přiřazení pomocí metody ''swap''. * Kopírující přiřazení se dá konceptuálně rozdělit na dva kroky, smazání starého obsahu a vytvoření kopie. Jedno jsme již naimplementovali, jak nám swap pomůže s tím druhým? {{:courses:b6b36pjc:cviceni:cviceni_6_swap.zip|Řešení}} ==== Další příklady ==== Abyste se utvrdili v tom, kdy se použije kopie, kdy přesun, a kdy ani jedno z toho, zkuste si projít jeden po druhém příklady v {{:courses:b6b36pjc:cviceni:cviceni_6_copy_move_lifetimes.zip|tomto souboru}} ([[https://coliru.stacked-crooked.com/a/dfbbaace0e427264|Coliru]]). Ještě před spuštěním si zkuste rozmyslet, které operace proběhnou.