Як тільки ви почнете регулярно використовувати семантику переміщення, ви зрозумієте наскільки корисною вона може бути.
Проблема
Дуже часто об’єкти, з якими вам доведеться працювати, будуть не r-values, а l-values. Наприклад, розглянемо наступний шаблон функції swap():
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
#include <iostream> #include <string> template<class T> void swap(T& x, T& y) { T tmp { x }; // викликає конструктор копіювання x = y; // викликає оператор присвоювання копіюванням y = tmp; // викликає оператор присвоювання копіюванням } int main() { std::string x{ "Anton" }; std::string y{ "Max" }; std::cout << "x: " << x << '\n'; std::cout << "y: " << y << '\n'; swap(x, y); std::cout << "x: " << x << '\n'; std::cout << "y: " << y << '\n'; return 0; } |
Приймаючи два об’єкти типу T
(в даному випадку std::string) функція swap() міняє місцями їх значення, роблячи при цьому три копії.
Відповідно, результат виконання програми:
x: Anton
y: Max
x: Max
y: Anton
А як ми вже дізналися на попередньому уроці, копіювання — це не дуже ефективно. А оскільки ми виконуємо копіювання тричі, то це ще й порівняно повільно. Ми можемо цього уникнути. Наша мета — поміняти місцями значення x
і y
. А для цього ми можемо використати переміщення замість копіювання, зробивши наш код більш продуктивним!
Але як? Проблема полягає в тому, що параметри x
і y
є посиланнями l-value, а не посиланнями r-value, тому у нас немає способу викликати конструктор переміщення або оператор присвоювання переміщенням замість конструктора копіювання і оператора присвоювання копіюванням. За замовчуванням у нас використовується семантика копіювання. Що робити?
Функція std::move()
Функція std::move() — це стандартна бібліотечна функція, яка конвертує переданий аргумент в r-value. Ми можемо передати l-value в функцію std::move(), і std::move() поверне нам посилання r-value. Для роботи з std::move() потрібно підключити заголовок utility.
Ось вищенаведена програма, але вже з функцією swap(), яка використовує std::move() для перетворення l-values в r-values, щоб ми мали можливість використати семантику переміщення замість семантики копіювання:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
#include <iostream> #include <string> #include <utility> template<class T> void swap(T& x, T& y) { T tmp { std::move(x) }; // викликає конструктор переміщення x = std::move(y); // викликає оператор присвоювання переміщенням y = std::move(tmp); // викликає оператор присвоювання переміщенням } int main() { std::string x{ "Anton" }; std::string y{ "Max" }; std::cout << "x: " << x << '\n'; std::cout << "y: " << y << '\n'; swap(x, y); std::cout << "x: " << x << '\n'; std::cout << "y: " << y << '\n'; return 0; } |
Результат виконання один і той же:
x: Anton
y: Max
x: Max
y: Anton
Але ця версія програми набагато ефективніша. При ініціалізації tmp
, замість створення копії x
, ми використовуємо std::move() для конвертації змінної x
, яка є l-value, в r-value. А оскільки параметром стає r-value, то за допомогою семантики переміщення x
переміщується в tmp
.
Потім, через кілька додаткових перестановок, значення змінної x
переміщується в змінну y
, а значення y
переміщується в змінну x
.
Ще один приклад
Ми також можемо використати std::move() для заповнення контейнерних класів (таких як std::vector) значеннями l-values.
У наступній програмі ми спочатку додаємо елемент в вектор, використовуючи семантику копіювання, а потім додаємо елемент в вектор, використовуючи семантику переміщення:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <iostream> #include <string> #include <utility> #include <vector> int main() { std::vector<std::string> v; std::string str = "Bye"; std::cout << "Copying str\n"; v.push_back(str); // викликає версію l-value методу push_back(), яка копіює str в елемент масиву std::cout << "str: " << str << '\n'; std::cout << "vector: " << v[0] << '\n'; std::cout << "\nMoving str\n"; v.push_back(std::move(str)); // викликає версію r-value методу push_back(), яка переміщує str в елемент масиву std::cout << "str: " << str << '\n'; std::cout << "vector: " << v[0] << ' ' << v[1] << '\n'; return 0; } |
Результат виконання програми:
Copying str
str: Bye
vector: Bye
Moving str
str:
vector: Bye Bye
У першому випадку ми передаємо l-value в push_back(), тому використовується семантика копіювання для додавання елемента в вектор. З цієї причини змінна str
залишається з колишнім значенням.
У другому випадку ми передаємо r-value (фактично l-value, яке конвертується в r-value через std::move()) в push_back(), тому використовується семантика переміщення для додавання елемента в вектор. Це більш ефективно, тому що елемент вектора може “вкрасти” значення змінної std::string, замість його копіювання. З цієї ж причини str
позбавляється свого значення.
На цьому етапі std::move() наче підказує компілятору, що нам більше не потрібен цей об’єкт (принаймні, в його поточному стані). Отже, ви не повинні використовувати std::move() з будь-яким «постійним» об’єктом, який ви не хочете змінювати, і вам не слід очікувати, що об’єкти, які використовуються з std::move(), залишаться колишніми.
Функції переміщення
Як ми вже говорили на попередньому уроці, ви повинні залишати об’єкти, ресурси яких ви переміщуєте, в чітко визначеному стані. В ідеалі це має бути «нульовий стан» (null/nullptr).
Тепер ми можемо поговорити про те, чому при використанні функції std::move(), об’єкт, ресурси якого переміщуються, може не бути тимчасовим. Справа в тому, що користувач може захотіти повторно використати цей же (тепер порожній) об’єкт або протестувати його будь-яким чином.
У прикладі, наведеному вище, рядок str
залишається порожнім після виконання переміщення (що завжди робить std::string після успішного переміщення). Таким чином, ми можемо повторно його використати, якщо, звичайно, захочемо, або проігнорувати, якщо він нам більше не потрібен.
Чим ще корисна функція std::move()?
Функція std::move() також може бути корисна при сортуванні елементів масиву. Більшість алгоритмів сортування (такі як «метод вибору» і «бульбашкове сортування») працюють шляхом заміни пар елементів. На попередніх уроках нам доводилося використовувати семантику копіювання для виконання таких замін. Тепер же ми можемо використати семантику переміщення, яка є ефективнішою.
Функція std::move() також може бути корисна при переміщенні вмісту з одного розумного вказівника в інший.
Висновки
Функція std::move() може використовуватися всякий раз, коли потрібно обробляти l-value як r-value з метою використання семантики переміщення замість семантики копіювання.