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í 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í.
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ří.
vector
u, třeba pomocí vector v2 = v;
. Co se stane?
vector
.
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:
return
.
std::move()
.
Příklady přesunů:
#include <vector> #include <iostream> void print_vector(const std::vector<int>& v) { for (auto& item : v) std::cout << item << ' '; std::cout << '\n'; } std::vector<int> get() { std::vector<int> v = { 1, 23, 4, 1 }; return v; } int main() { std::vector<int> v1 = get(); // v přesunut do v1 std::vector<int> 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.
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ý.
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 |
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 =
.
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;
.
rhs
je tentýž objekt, jako *this
? Pokud se stane něco špatného, funkci opravte.
vector& operator=(vector&&)
.
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); }
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
.
vector
metodu swap
, která vymění obsah dvou vektorů.
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.
std::swap
z hlavičky <utility>
.
swap
.
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 tomto souboru (Coliru). Ještě před spuštěním si zkuste rozmyslet, které operace proběhnou.