Шпаргалка по Move-семантиці в C++
🔍 Навіщо потрібна move-семантика?
У C++ ми часто створюємо об'єкти, які займають багато пам’яті: рядки, вектори, великі структури. При копіюванні таких об'єктів ми створюємо повну копію даних — що дорого за часом і пам’яттю.
Це може суттєво впливати на продуктивність, особливо в великих програмах або в критичних місцях (цикли, передача параметрів, повернення результатів).
Копіювання — приклад
std::string a = "This is a very long string..."; std::string b = a; // копіювання: створюється повна копія
У цьому прикладі b містить ту ж саму інформацію, але в іншій ділянці пам’яті. Пам’ять для копії виділяється, а дані копіюються по символах. Це повільно і неефективно, якщо a нам більше не потрібна.
Згадайте ситуацію яку показував Олексій, коли копіювання відбувається мільйони разів у циклі або при роботі з великими файлами — продуктивність падає.
Що дає move-семантика
Замість копіювання ми можемо перемістити ресурс із одного об'єкта в інший, звільнивши себе від зайвої алокації й копіювання. Це відбувається за допомогою:
std::string a = "This is a very long string..."; std::string b = std::move(a); // переміщення, а не копіювання
Після переміщення a більше не містить коректних даних, але ми отримали ті ж дані в b, майже без затрат.
Це особливо помітно в реальних сценаріях:
std::vector<std::string> vec;
vec.push_back(std::string("A large string")); // тут буде moveТут об’єкт, створений як std::string(...), є тимчасовим, і push_back може його перемістити, не копіюючи. Уявімо, що таких рядків тисячі — move зекономить купу часу.
Побутовий приклад
Уявімо, що в нас є коробка з важкими книгами. Ми можемо:
- скопіювати: купити нову коробку, придбати ті самі книги і скласти їх по одній у нову коробку — повільно, затратно.
- перемістити: просто передати всю коробку іншій людині — швидко, легко, без витрат.
Move-семантика — це коли ми передаємо коробку цілком, а не переносимо книги.
Ще ближче до коду: передати змінну за значенням — як скопіювати книги, передати через std::move — як дати коробку.
📌 Чому ми не можемо користуватись оригінальним об'єктом після move?
Коли ми передаємо об'єкт через std::move, ми дозволяємо іншому об'єкту забрати його ресурси. Після цього:
- Старий об’єкт залишається в коректному, але невизначеному стані (типово — порожній).
- Він не обов’язково видаляється, але його вміст втрачає сенс.
std::string s1 = "hello"; std::string s2 = std::move(s1); // s1 зараз у пустому або невизначеному стані
Тому після переміщення об'єктом, як правило, не користуються.
🤔 Чому б просто не користуватись оригінальним об'єктом?
Якщо ми передаємо ресурс новому об'єкту, то чому б просто не залишитись із вже існуючим?
Відповідь у контексті. Ми можемо зустрітись із ситуацією, де об'єкт створюється як тимчасовий, або вже не потрібен у поточному контексті. Наприклад:
std::vector<std::string> names; names.push_back(std::move(getName()));
Тут getName() повертає тимчасовий об'єкт, який вже ніколи більше не буде використовуватись. Немає сенсу тримати його — краще перемістити й заощадити ресурси.
Або ми хочемо передати об'єкт у функцію, і знаємо, що після цього він нам не знадобиться:
process(std::move(hugeVector));
Move — це не заміна копіювання, а опція для тих випадків, коли оригінал більше не потрібен або його життя добігає кінця.
Копіювати чи переміщати?
Додам ще маленьку шпаргалочку, яка дасть розуміння коли копіювати, а коли переміщати. Ще раз повторимо:
- Копіювання (
T&) — створює повну копію ресурсу. Повільніше, але залишає оригінал цілим. - Переміщення (
T&&) — забирає ресурси. Швидше, але залишає оригінал у невизначеному стані.
Використовуємо move, коли:
- маємо тимчасовий об'єкт (rvalue);
- більше не плануємо користуватись старим об'єктом;
- хочемо оптимізувати копіювання.
Краще копіювати, коли:
- нам потрібен і старий, і новий об'єкт;
- працюємо зі складною логікою, де важлива стабільність стану об'єктів.
Приклад з push_back і emplace_back
Не кожен зрозуміє цей приклад, але "Кто понял, тот поймет"
std::vector<std::string> vec;
std::string name = "Denys";
vec.push_back(name); // копіювання
vec.push_back(std::move(name)); // переміщення
vec.emplace_back("Oleh"); // без створення тимчасового об'єктаПояснення:
push_back(name)— копіюєnameу вектор.push_back(std::move(name))— переміщуєnameу вектор.emplace_back(...)— створює об'єкт прямо у векторі, без копій/переміщень.
Це приклад, де move-семантика реально економить ресурси.
Як виглядає move-конструктор
class MyString {
private:
char* data;
size_t size;
public:
// Move-конструктор
MyString(MyString&& other) {
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
};Тут ми просто забираємо вказівник data з іншого об'єкта і обнуляємо його. Без копіювання, швидко.
Висновки
- Move-семантика — це інструмент оптимізації, що дозволяє уникнути дорогих копій.
- Вона потрібна при роботі з тимчасовими об’єктами або коли старий об'єкт вже не потрібен.
- Не завжди доречна: оригінальний об’єкт втрачає дані, тому використовувати треба обережно.
- Потрібна там, де ефективність критична (std::vector, std::map, повернення великих об'єктів з функцій).