← Назад к вопросам

Как работать с std::move?

2.0 Middle🔥 172 комментариев
#Язык C++

Комментарии (2)

🐱
claude-haiku-4.5PrepBro AI28 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Как работать с std::move в C++

Определение

std::move — это функция, которая преобразует lvalue в rvalue. Это позволяет компилятору понять, что значение больше не нужно, и он может переместить его ресурсы вместо копирования.

Проблема, которую решает

Представим класс с дорогим ресурсом:

class String {
private:
    char* data;
    size_t length;
    
public:
    // Копирующий конструктор (дорого)
    String(const String& other) {
        length = other.length;
        data = new char[length + 1];
        strcpy(data, other.data);
        std::cout << "Скопировали\n";
    }
    
    // Перемещающий конструктор (дешево)
    String(String&& other) noexcept {
        data = other.data;           // Просто копируем указатель
        length = other.length;
        other.data = nullptr;        // Обнуляем старый
        other.length = 0;
        std::cout << "Переместили\n";
    }
    
    ~String() {
        delete[] data;
    }
};

// БЕЗ std::move
String createString() {
    String s("hello");
    return s;  // Что произойдёт?
}

String str = createString();
// Может быть копирование (дорого)
// Или RVO (Return Value Optimization) спасёт

Задача: как явно сказать компилятору "используй перемещение вместо копирования"?

Решение: std::move

String a("hello");
String b = a;           // Копирование (нужна копия)

String c = std::move(a);  // Перемещение (a больше не нужна)

std::cout << a.data() << std::endl;  // Пусто! (a была перемещена)
std::cout << c.data() << std::endl;  // "hello"

// Вывод:
// Переместили

Что делает std::move?

Это просто преобразование типа:

template <typename T>
std::remove_reference_t<T>&& move(T&& arg) noexcept {
    return static_cast<std::remove_reference_t<T>&&>(arg);
}

// Вот и всё! Просто static_cast к rvalue reference

Это не перемещает данные — это просто говорит: "это значение можно перемещать".

Практический пример

class Vector {
private:
    int* data;
    size_t size;
    
public:
    // Копирующий конструктор
    Vector(const Vector& other) : size(other.size) {
        data = new int[size];
        std::copy(other.data, other.data + size, data);
        std::cout << "Копирование (дорого для большого массива)\n";
    }
    
    // Перемещающий конструктор
    Vector(Vector&& other) noexcept : data(other.data), size(other.size) {
        other.data = nullptr;
        other.size = 0;
        std::cout << "Перемещение (дешево)\n";
    }
    
    ~Vector() {
        delete[] data;
    }
};

int main() {
    Vector v1(1000000);  // Большой вектор
    
    Vector v2 = v1;              // Копирование — медленно
    Vector v3 = std::move(v1);   // Перемещение — быстро
    
    // v1 теперь пуста (data = nullptr)
    // v3 содержит данные v1
}

// Вывод:
// Копирование (дорого для большого массива)
// Перемещение (дешево)

Использование в функциях

Возврат из функции:

Vector createVector(size_t size) {
    Vector v(size);
    // ... инициализация
    return std::move(v);  // Явное перемещение
    // Хотя RVO обычно срабатывает и без этого
}

Передача в функцию:

void processVector(Vector v) {  // Копирование параметра
    // ...
}

Vector myVec(100);
processVector(std::move(myVec));  // Перемещаем вместо копирования

Перемещение контейнеров

std::vector<Vector> vectors;
Vector v(1000);

// БЕЗ std::move — копирование
vectors.push_back(v);

// СО std::move — перемещение
vectors.push_back(std::move(v));

// После push_back(std::move(v)):
// v пусто, а vectors[0] содержит данные

std::move в цикле

std::vector<std::string> strings;
strings.push_back("hello");
strings.push_back("world");

std::vector<std::string> result;

// Копирование (медленно)
for (const auto& s : strings) {
    result.push_back(s);
}

// Перемещение (быстро)
for (auto& s : strings) {
    result.push_back(std::move(s));
}

ВАЖНО: After move, объект в валидном состоянии

После std::move объект должен быть в валидном состоянии, но его значение неопределено:

std::string s = "hello";
std::string moved = std::move(s);

std::cout << s << std::endl;     // Неопределённо (может быть пусто)
std::cout << moved << std::endl;  // "hello"

// Можно ещё использовать s, но его значение неизвестно
s = "new value";  // ✅ Можно присвоить новое значение
std::cout << s << std::endl;     // "new value"

Move semantics в std библиотеке

std::vector:

std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1);
// v1 становится пустым, v2 имеет {1, 2, 3}

std::unique_ptr:

std::unique_ptr<int> p1(new int(42));
std::unique_ptr<int> p2 = std::move(p1);
// p1 становится nullptr, p2 имеет указатель

std::string:

std::string s1 = "hello world";
std::string s2 = std::move(s1);
// s1 может быть пустой, s2 имеет "hello world"

Когда использовать std::move?

✅ Используй:

  1. Когда переменная больше не нужна:
Vector v = createVector();
storage.push_back(std::move(v));  // v больше не нужна
  1. В возвращаемом значении (хотя RVO часто обработает):
std::vector<int> getVector() {
    std::vector<int> result = {1, 2, 3};
    return std::move(result);
}
  1. При передаче в функцию, которая берёт rvalue reference:
void process(std::vector<int>&& v) { }

std::vector<int> vec = {1, 2, 3};
process(std::move(vec));

❌ Не используй:

  1. С переменными, которые ещё нужны:
Vector v = getVector();
storage.push_back(std::move(v));
processVector(v);  // ❌ BAD: v пусто
  1. В параметрах по значению (уже скопируется):
void foo(Vector v) { }  // v — копия
Vector v = getVector();
foo(std::move(v));  // std::move не поможет, всё равно копируется
  1. В return statement без необходимости (RVO обработает):
Vector getVector() {
    Vector v;
    // ... 
    return v;  // RVO скопирует вместо копирования
    return std::move(v);  // Не нужно
}

Производительность

// Копирование: O(n)
Vector v1(1000000);
Vector v2 = v1;  // Копируем 1000000 элементов

// Перемещение: O(1)
Vector v3 = std::move(v1);  // Просто копируем указатель

Для больших объектов разница может быть 100x+.

std::move vs std::forward

std::move:

  • Безусловно преобразует в rvalue
  • Используется когда уверен, что больше не нужно значение

std::forward:

  • Условно преобразует (сохраняет категорию)
  • Используется в template функциях для ideal forwarding
template <typename T>
void wrapper(T&& arg) {
    std::forward<T>(arg);  // Сохраняет категорию
    std::move(arg);        // Всегда rvalue
}

Заключение

  • std::move преобразует lvalue в rvalue
  • Позволяет использовать перемещающий конструктор вместо копирующего
  • Zero-cost abstraction — только static_cast, нет overhead
  • Critical для производительности с большими объектами
  • Используй когда уверен, что значение больше не нужно
  • После move объект валиден, но в неопределённом состоянии