Search
Kopie a přesuny jsou nejběžnější operace v každém programovacím jazyce. Často si ani neuvědomujeme, že tyto operace využíváme a automaticky předpokládáme, že se vykonají správně. Vzpomeňme například operaci přiřazení =. Vykonalo se přiřazení vždy, když jste ho použili správně? Podívejme se na příklad v jazyce Python.
=
Vzpomeňme na seznamy definované v Pythonu, například:
old_list = [[1, 2, 3], [4, 5, 'a'], [7, 8]]
new_list = old_list
new_list
old_list
new_list[1][2] = 9
Old List: [[1, 2, 3], [4, 5, 9], [7, 8]] New List: [[1, 2, 3], [4, 5, 9], [7, 8]]
new_list.append('C++')
Old List: [[1, 2, 3], [4, 5, 9], [7, 8], 'C++'] New List: [[1, 2, 3], [4, 5, 9], [7, 8], 'C++']
new_list.pop(1)
Old List: [[1, 2, 3], [7, 8], 'C++'] New List: [[1, 2, 3], [7, 8], 'C++']
Pro nezávislou práci s oběma seznamy potřebujeme vytvořit kopii původního seznamu:
new_list = copy.deepcopy(old_list)
Old List: [[1, 2, 3], [4, 5, 'a'], [7, 8]] New List: [[1, 2, 3], [4, 5, 9], [7, 8]]
Old List: [[1, 2, 3], [4, 5, 'a'], [7, 8]] New List: [[1, 2, 3], [4, 5, 9], [7, 8], 'C++']
Old List: [[1, 2, 3], [4, 5, 'a'], [7, 8]] New List: [[1, 2, 3], [7, 8], 'C++']
V C++ lze kopie vytvářet přímo přiřazovacím příkazem. Způsob, jak kopii vytvořit, definuje kopírující konstruktor.
Zopakujme si, jaké typy proměnných a parametrů se mohou vyskytovat v jazyce C++.
b
const T
a
*a
T
T a = b;
void f(T a); f(b);
T*
T* a = &b;
void f(T* a); f(&b);
const T*
const T* a = &b;
void f(const T* a); f(&b);
T&
T& a = b;
void f(T& a); f(b);
const T&
const T& a = b;
void f(const T& a); f(b);
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á zajišť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
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:
= delete
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ří.
std::unique_ptr
unique_ptr
vector v2 = v;
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.
v1
v2
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.
std::move
T&&
vector&&
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:
T&& a = std::move(b);
void f(T&& a); f(std::move(b));
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:
vectoru
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& operator=(const vector& rhs)
rhs
return *this;
*this
vector& operator=(vector&&)
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); }
Řešení
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.
swap
void swap(vector& rhs)
std::swap
<utility>
Zamyslete se nad rozdíly při kopírování řetězce, resp. pole znaků. Jaký je obsah proměnné zoz po vykonání následujícího kódu?
zoz
std::string b1 = "Ahoj"; std::string* arr1 = new std::string[3]; for (int i = 0; i < 3; ++i) arr1[i] = b1; arr1[0][1] = 'c'; std::cout << arr1[0] << arr1[1] << arr1[2] << std::endl; delete[] arr1;
char b2[5] = { 'A', 'h', 'o', 'j', '\0' }; char** arr2 = new char*[3]; for (int i = 0; i < 3; ++i) arr2[i] = b2; arr2[0][1] = 'c'; std::cout << arr2[0] << arr2[1] << arr2[2] << std::endl; delete[] arr2;
Jak se změní chování programů pokud místo kopírování použijeme přesun?
arr[i] = std::move(b);
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.
V zjednodušené verzi si příklady můžete projít na interaktivní webové stránce. U každého příkladu určete, jak bude vypadat výslední výpis. Zamyslete se nad tím, který řádek hlavního programu (na stránce uprostřed) způsobí kterou část výpisu, a tedy volání příslušného konstruktoru.