Введение
До C++11 копирование было единственным способом передать объект «по значению»: если функция возвращала std::vector<int> из миллиона элементов, компилятор (в лучшем случае с помощью оптимизаций) копировал весь буфер. Move-семантика, появившаяся в C++11, ввела понятие «перемещения» — передачи владения ресурсом без его копирования. std::move — это инструмент, который явно говорит компилятору: «мне больше не нужно это значение, можешь забрать его ресурсы».
Концепция
Move-семантика строится на:
- rvalue-ссылках (
T&&) — ссылки, которые могут связываться с временными объектами; - move-конструкторе и move-операторе присваивания — специальных перегрузках, которые «опустошают» источник, забирая его внутренние ресурсы (указатели, буферы) вместо их копирования;
std::move— это не функция, которая что-то перемещает физически, а простоstatic_cast<T&&>, то есть приведение lvalue к rvalue-ссылке, разрешающее компилятору выбрать move-перегрузку.
После std::move(obj) объект obj находится в «валидном, но неопределённом» состоянии — использовать его дальше можно только после повторной инициализации или вызова методов, явно гарантирующих корректность (например, clear()).
Пример кода
#include <iostream>
#include <vector>
#include <utility>
#include <string>
class Buffer {
public:
explicit Buffer(size_t size) : size_(size), data_(new int[size]) {
std::cout << "Constructed buffer of size " << size_ << "n";
}
// Move-конструктор: забираем указатель, обнуляем источник
Buffer(Buffer&& other) noexcept
: size_(other.size_), data_(other.data_) {
other.size_ = 0;
other.data_ = nullptr;
std::cout << "Moved buffern";
}
// Move-оператор присваивания
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
// Копирующий конструктор: реальное копирование данных (для сравнения)
Buffer(const Buffer& other) : size_(other.size_), data_(new int[other.size_]) {
std::copy(other.data_, other.data_ + size_, data_);
std::cout << "Copied buffer of size " << size_ << "n";
}
Buffer& operator=(const Buffer&) = delete; // упростим пример
~Buffer() {
delete[] data_;
}
size_t size() const { return size_; }
private:
size_t size_;
int* data_;
};
Buffer makeBuffer(size_t size) {
Buffer b(size);
return b; // компилятор использует move (или даже RVO/NRVO)
}
int main() {
std::vector<Buffer> buffers;
buffers.reserve(3); // избегаем лишних перемещений при росте вектора
buffers.push_back(makeBuffer(100)); // временный объект -> move
Buffer named = makeBuffer(200);
buffers.push_back(std::move(named)); // явный move именованного объекта
std::cout << "named.size() after move = " << named.size() << "n"; // 0, валидно, но опустошено
// std::move в реализации swap
Buffer a(10), b(20);
Buffer tmp = std::move(a);
a = std::move(b);
b = std::move(tmp);
std::cout << "After swap: a.size()=" << a.size() << ", b.size()=" << b.size() << "n";
return 0;
}
Пояснения к коду
Buffer владеет сырым массивом int*. Move-конструктор и move-оператор присваивания просто копируют указатель и обнуляют источник — никакого выделения памяти, никакого копирования содержимого, операция выполняется за константное время независимо от размера буфера. Копирующий конструктор оставлен для контраста: он выделяет новую память и копирует все элементы — операция O(n).
В makeBuffer возврат локального объекта b по значению либо подвергается RVO/NRVO (оптимизация исключает копирование/перемещение вовсе), либо, если оптимизация не сработала, компилятор автоматически трактует возврат локальной переменной как rvalue и вызывает move-конструктор — никакого std::move в return писать не нужно (и даже вредно, так как это может помешать NRVO).
Вызов buffers.push_back(std::move(named)) — пример явного перемещения: named — lvalue, и без std::move компилятор выбрал бы копирующий конструктор. После этой строки named гарантированно в опустошённом состоянии (size() == 0), и любое использование как полноценного буфера было бы логической ошибкой (хотя деструктор и size() работают безопасно, потому что класс написан корректно).
Подводные камни и советы
1. Не используйте объект после std::move (кроме как для переинициализации или вызова безопасных методов) — это источник трудноуловимых ошибок. 2. std::move ничего не делает сам по себе — это просто приведение типа. Реальное «перемещение» происходит только если у класса определён move-конструктор/move-оператор; иначе будет вызвано обычное копирование. 3. Move-операции должны быть noexcept, когда это возможно — иначе std::vector при изменении размера будет использовать копирование вместо перемещения (чтобы сохранить строгую гарантию безопасности исключений). 4. Не злоупотребляйте std::move при возврате локальных переменных — это может отключить NRVO. Компилятор сам разберётся. 5. std::move на const объекте бесполезен — const T&& всё равно резолвится в копирующий конструктор, потому что move-конструктор обычно принимает неконстантную rvalue-ссылку.
================================================================================