Проблема, которую решало перемещение
До C++11 любое «отдать объект» означало «скопировать». Вернёте из функции вектор на миллион элементов — получите глубокую копию. Поместите тяжёлый объект в контейнер — ещё одну. Компиляторы умели RVO, но оно срабатывало не всегда.
Move-семантика добавила в язык идею: если источник больше не нужен, вместо копирования украдём у него ресурсы. Скопировать указатель на кучу, обнулить источник — и вместо копирования миллиона элементов у нас три машинных инструкции.
std::vector<int> a = fillVector(); // C++11+: ресурсы переедут, а не скопируются
std::vector<int> b = std::move(a); // b забрал буфер, a стал пустым
lvalue и rvalue
Чтобы перемещение работало, компилятору нужно знать: этот объект ещё пригодится или его можно «разобрать на запчасти».
- lvalue — выражение с именем, на него можно взять адрес: переменные,
*ptr,v[i],a.b. - rvalue — временное: результат функции по значению, литерал,
x + y,std::move(x).
Ссылка T&& (rvalue-ссылка) может привязываться только к rvalue — и это механизм выбора перегрузок:
void f(std::string& s); // вызывается для lvalue — копировать нельзя
void f(std::string&& s); // вызывается для rvalue — можно воровать
std::string name = "x";
f(name); // lvalue → первая версия
f(std::string("tmp")); // временный → вторая
f(std::move(name)); // принудительно превратили в rvalue
std::move — это просто приведение типа
std::move ничего не перемещает. Это static_cast<T&&> — «пометь объект как расходник». Реальное перемещение делает move-конструктор или move-оператор присваивания вызываемой стороны.
std::string src = "большие данные";
std::string dst = std::move(src);
// перемещение сделал конструктор string(string&&): он забрал буфер
// src валиден, но в "перемещённом" состоянии — обычно пуст
После std::move на вас лежит обязанность: не использовать перемещённый объект, кроме операций, безопасных для любого состояния — присвоить новое значение, вызвать clear(), деструировать. Читать его содержимое нельзя — там «неуточнённое, но валидное» состояние.
Пишем класс с move-семантикой: правило пяти
Если класс владеет ресурсом, ему нужны пять функций — деструктор, копирующая пара и перемещающая пара. Это «правило пяти» (бывшее «правило трёх» до C++11):
class Buffer {
char* data_ = nullptr;
size_t size_ = 0;
public:
explicit Buffer(size_t n) : data_(new char[n]), size_(n) {}
~Buffer() { delete[] data_; }
// копирование — глубокое
Buffer(const Buffer& o) : data_(new char[o.size_]), size_(o.size_) {
std::copy(o.data_, o.data_ + size_, data_);
}
Buffer& operator=(const Buffer& o) {
if (this != &o) {
auto* fresh = new char[o.size_];
std::copy(o.data_, o.data_ + o.size_, fresh);
delete[] data_; // сначала новое — и только потом удаляем старое
data_ = fresh; size_ = o.size_;
}
return *this;
}
// перемещение — воруем указатель
Buffer(Buffer&& o) noexcept : data_(o.data_), size_(o.size_) {
o.data_ = nullptr; o.size_ = 0;
}
Buffer& operator=(Buffer&& o) noexcept {
if (this != &o) {
delete[] data_;
data_ = o.data_; size_ = o.size_;
o.data_ = nullptr; o.size_ = 0;
}
return *this;
}
};
Обратите внимание на noexcept у перемещающих операций: контейнеры вроде vector при реаллокации выбирают move только если он noexcept, иначе используют копию ради строгой гарантии исключений. Забыть noexcept — значит молча замедлить свой класс.
Где move-семантика срабатывает сама
- Возврат по значению из функции — автоматически rvalue, перемещение или RVO без копии.
push_back(std::move(obj))— объект переезжает в контейнер.std::swap— реализован через три перемещения.- Параметры-«воронки»:
void setName(std::string n)— вызывающий решает, копировать (setName(name)) или перемещать (setName(std::move(name))).
Perfect forwarding и && в шаблонах
В шаблонах T&& — это не «rvalue-ссылка», а forwarding reference: схлопывается по правилам вывода и может стать и lvalue-ссылкой. Чтобы передать аргумент дальше с сохранением категории, используют std::forward:
template<class T>
void wrapper(T&& arg) {
realWorker(std::forward<T>(arg)); // rvalue останется rvalue, lvalue — lvalue
}
Именно так работают emplace_back, make_unique, bind — принимают аргументы и прокидывают в конструктор без лишних копий.
Типичные ошибки
std::moveна возврате локальной переменной.return std::move(local);— мешает RVO! Простоreturn local;— компилятор сам переместит или вообще сконструирует в месте вызова.- Использование после move. Классика: цикл по вектору после
std::move(v). - Move-операции без
noexcept— тихая деградация производительности. - Константные источники.
std::moveотconstобъекта всё равно приводит к копии — move-конструктор хочет неконстантныйT&&. - Перемещение базовых типов.
std::moveдляint— просто копия, воровать нечего.
Итог
Move-семантика — это про дешёвое «отдать» вместо дорогого «скопировать». Сигнатуры T&&, std::move как «метка расходника», noexcept на перемещающих операциях и правило пяти для классов-владельцев. А главная привычка — не std::move на выходе из функции и не использовать объекты после перемещения.
Проверка: видно ли перемещение
Напишите класс с печатью в конструкторах — и посмотрите, когда что вызывается:
struct Verbose {
Verbose() { puts("ctor"); }
Verbose(const Verbose&) { puts("copy"); }
Verbose(Verbose&&) noexcept { puts("move"); }
~Verbose() { puts("dtor"); }
};
Verbose a; // ctor
Verbose b = a; // copy
Verbose c = std::move(a); // move
Увидите на практике, где копии, где перемещения — лучший способ почувствовать семантику.
copy-and-swap: элегантное присваивание
Единая идиома для копирующего и перемещающего присваивания через swap:
void swap(Buffer& a, Buffer& b) noexcept {
std::swap(a.data_, b.data_);
std::swap(a.size_, b.size_);
}
Buffer& operator=(Buffer o) noexcept { // по значению — копия или move на входе
swap(*this, o);
return *this;
}
Параметр по значению: для lvalue он копируется, для rvalue — перемещается. Одна реализация покрывает оба случая и строгую гарантию исключений заодно.
Практическая метрика
На больших объектах разница измерима: копия строки на 1 МБ — аллокация + memcpy, перемещение — перестановка трёх указателей. В цикле из тысячи операций это разница между миллисекундами и микросекундами. Пишите бенчмарк через std::chrono — убедитесь сами.
Что почитать дальше
- Rule of Zero / Rule of Five — когда классам вообще не нужны эти функции.
std::forwardи forwarding references — для шаблонов и perfect forwarding.- Move-only типы:
unique_ptr,thread,fstream— их нельзя копировать, только перемещать.