March 26

Шпаргалка по 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, повернення великих об'єктів з функцій).