специфика C++ — управление ресурсами без сборщика мусораC++-specific — resource management without a garbage collector
std::move ничего не двигает — это каст к rvalue-ссылке. Само перемещение делает конструктор/оператор присваивания, который эту rvalue-ссылку принимает и решает забрать ресурс вместо его копирования. Разница между "выглядит как перемещение" и "действительно перемещает" — источник большинства ошибок в этой теме.std::move doesn't move anything — it's a cast to an rvalue reference. The actual move is performed by the constructor/assignment operator that accepts that rvalue reference and decides to steal the resource instead of copying it. The gap between "looks like a move" and "actually moves" is the source of most mistakes in this topic.
Move-семантика — механизм передачи владения ресурсом от одного объекта другому без копирования содержимого: вместо аллокации нового буфера и поэлементного копирования, указатель на существующий буфер просто переходит новому владельцу, а старый обнуляется. std::move(x) сам по себе НЕ перемещает x — это чистый каст типа static_cast<T&&>(x), превращающий lvalue в rvalue-ссылку, чтобы включить перегрузку с move-семантикой при разрешении вызова.Move semantics is a mechanism for transferring resource ownership from one object to another without copying the contents: instead of allocating a new buffer and copying element by element, the pointer to the existing buffer simply passes to the new owner, and the old one is nulled out. std::move(x) by itself does NOT move x — it's a pure cast, equivalent to static_cast<T&&>(x), turning an lvalue into an rvalue reference so that overload resolution picks the move-semantics overload.
std::string a = "hello";
std::string b = a; // copy: b получает собственную копию буфера// copy: b gets its own copy of the buffer
std::string c = std::move(a); // move: c забирает буфер a; a теперь валиден, но не определён (обычно пуст)// move: c steals a's buffer; a is now valid but unspecified (usually empty)
"Валиден, но не определён" (valid but unspecified state) — формальная гарантия стандарта после перемещения ИЗ объекта: на нём по-прежнему можно безопасно вызывать методы, разрушать его, присваивать ему новое значение — но нельзя полагаться на КОНКРЕТНОЕ содержимое, которое там осталось."Valid but unspecified state" is the standard's formal guarantee for an object that's been moved FROM: it's still safe to call methods on it, destroy it, assign a new value to it — but you can't rely on the SPECIFIC contents left behind.
Return Value Optimization строит возвращаемый объект сразу в памяти вызывающей стороны, минуя и копирование, и перемещение целиком. Для prvalue (временного объекта, создаваемого прямо в return) это гарантировано стандартом с C++17 — не просто разрешённая оптимизация, а обязательное поведение.Return Value Optimization constructs the returned object directly in the caller's memory, bypassing both copying and moving entirely. For a prvalue (a temporary created right in the return expression), this has been guaranteed by the standard since C++17 — not just a permitted optimization, but mandatory behavior.
std::string makeString() {
return std::string("hello"); // prvalue: с C++17 гарантированно ни copy, ни move// prvalue: since C++17, guaranteed neither copy nor move
}
std::string makeStringNamed() {
std::string local = "hello";
return local; // NRVO (named RVO): разрешена, но не гарантирована — компилятор обычно её делает// NRVO (named RVO): permitted, not guaranteed — compilers usually apply it
} // НЕ писать return std::move(local) — это мешает NRVO и в лучшем случае даёт move вместо неё// don't write return std::move(local) — it defeats NRVO and at best gives a move instead
Разница между гарантированным (C++17, prvalue) и негарантированным (NRVO, именованная локальная переменная) случаями важна: RVO для prvalue — не оптимизация, которую компилятор МОЖЕТ применить, а часть модели языка (у самого временного объекта в этом случае формально нет отдельной копии для устранения). NRVO для именованной переменной — оптимизация, которую качественный компилятор почти всегда применяет, но которая формально не гарантирована стандартом.The difference between the guaranteed (C++17, prvalue) and non-guaranteed (NRVO, named local) cases matters: RVO for a prvalue isn't an optimization the compiler MAY apply — it's part of the language model (there's formally no separate copy of the temporary to eliminate in this case). NRVO for a named variable is an optimization a quality compiler almost always applies, but one the standard doesn't formally guarantee.
Явный return std::move(local); — распространённая, но контрпродуктивная привычка: она превращает потенциальный NRVO (ноль операций) в гарантированный move (одна операция, пусть и дешёвая) и в некоторых компиляторах прямо мешает применению NRVO, поскольку выражение возврата перестаёт быть простым именем локальной переменной.Writing return std::move(local); explicitly is a common but counterproductive habit — it turns a potential NRVO (zero operations) into a guaranteed move (one operation, however cheap), and on some compilers directly defeats NRVO, since the return expression is no longer a plain local-variable name.
Выгода move-семантики пропорциональна размеру ресурса, который можно "украсть" одним присваиванием указателя вместо поэлементного копирования. Для типа без владения внешним ресурсом (например, struct Point { double x, y; };) move-конструктор физически не может делать ничего иного, кроме почленного копирования — перемещать нечего, есть только сами данные.The benefit of move semantics is proportional to the size of the resource that can be "stolen" with one pointer assignment instead of an element-by-element copy. For a type that owns no external resource (say, struct Point { double x, y; };), the move constructor can physically do nothing other than a member-wise copy — there's nothing to move, only the data itself.
struct Point { double x, y; }; // move конструктор == copy конструктор по факту — оба просто копируют 16 байт// move constructor == copy constructor in effect — both just copy 16 bytes
class BigBuffer {
std::vector<double> data; // потенциально мегабайты — здесь move даёт реальный выигрыш// potentially megabytes — here a move gives a real payoff
};
Отсюда практический вывод: move-семантика окупается для типов, владеющих ресурсом переменного и потенциально большого размера (буферы, строки, контейнеры, файловые дескрипторы) — не как универсальное "move всегда быстрее copy", а как оптимизация конкретно того случая, где копирование содержимого дорого, а перемещение владения — нет.The practical takeaway: move semantics pays off for types owning a variable and potentially large resource (buffers, strings, containers, file handles) — not as a blanket "move is always faster than copy," but as an optimization for the specific case where copying the contents is expensive and transferring ownership isn't.
Читать состояние объекта, из которого только что переместили, — не undefined behavior (гарантия "valid but unspecified" это разрешает), но почти всегда логическая ошибка: код, полагающийся на конкретное содержимое перемещённого объекта, хрупок и часто ломается при смене реализации типа или компилятора.Reading the state of an object that was just moved from isn't undefined behavior (the "valid but unspecified" guarantee permits it), but it's almost always a logic error: code relying on the moved-from object's specific contents is fragile and often breaks when the type's implementation or the compiler changes.
const std::string s = "hello";
std::string t = std::move(s); // std::move(s) даёт const std::string&&, move-конструктор его не принимает — вызывается copy// std::move(s) yields const std::string&&, the move constructor won't bind to it — copy is called
Move-конструктор принимает T&& (неconst rvalue-ссылку), а std::move на const T даёт const T&& — эта ссылка не связывается с параметром move-конструктора, зато прекрасно связывается с const T& у copy-конструктора, так что вызов молча откатывается к копированию без единой ошибки или предупреждения.The move constructor takes T&& (a non-const rvalue reference), while std::move on a const T produces const T&& — that reference won't bind to the move constructor's parameter, but binds perfectly fine to the copy constructor's const T&, so the call silently falls back to copying with no error or warning at all.
Если на объект где-то ещё держится ссылка/указатель, ожидающий стабильного содержимого, перемещение ИЗ него неожиданно опустошает то, что видит другой код — move не проверяет, наблюдает ли кто-то ещё за объектом, в отличие от, например, shared_ptr, который явно считает наблюдателей.If a reference/pointer is held elsewhere expecting stable content, moving FROM the object unexpectedly empties out what that other code sees — a move doesn't check whether anyone else is observing the object, unlike, say, shared_ptr, which explicitly counts observers.