Copying a large buffer duplicates every byte. Transferring it rewrites a few pointers. For a vector of ten million integers, the copy moves 40 MB through memory bandwidth over milliseconds. The transfer writes three pointers in about five nanoseconds.
Move semantics is the C++ mechanism for choosing the transfer. It lets a constructor or assignment steal the resources of an object the caller no longer needs. The source stays valid but unspecified, usually empty.
This article explains which objects can be stolen from, how the transfer runs, and the one annotation that decides whether containers move or copy.
Some expressions persist, others expire
Every C++ expression belongs to a value category. An lvalue names a persistent object with an address. You can write &x for it. A variable name is the classic example.
An rvalue is a temporary near the end of its life. The literal 42 is one. The result of x + 1 is another. A function that returns by value produces one. Temporaries have no address you can keep.
References split along the same line. A plain & reference binds to lvalues. An rvalue reference, spelled &&, binds to temporaries. That distinction is the whole mechanism: an && parameter advertises "the caller is done with this object", which grants permission to steal its resources.
The same handoff idea in three languages shows why C++ needs the extra machinery:
#include <vector> #include <utility> // Hand a million-element buffer to a new owner without copying it. // The transfer rewrites three pointers: size, capacity, and data. void demo() { // A buffer holding 1,000,000 ints: about 4 MB on the heap. std::vector<int> a(1000000, 7); // std::move casts a to an rvalue: permission to steal, nothing more. std::vector<int> b = std::move(a); // b owns the buffer now: O(1) // a is valid but unspecified here: treat it as empty, never read it. b[0] = 42; // the buffer is alive and well under b } // b dies: the buffer frees exactly once
Java and Python rebind a handle, which never copies. C++ values own their storage directly, so rebinding the name is not enough: the language needs an explicit ownership transfer. That transfer is what the rest of this article builds.
std::move casts, the constructor performs the theft
std::move moves nothing. It is a cast that turns its argument into an rvalue reference. The cast makes the T&& overload selectable. The actual transfer happens inside the move constructor or move assignment.
A move constructor for a string-like type steals the buffer pointer, then parks the source in a safe empty state:
move source buffer pointer -> destination takes it over
set source pointer to null, length to zero -> source destructor stays safeThe moved-from object must remain destructible and assignable. Setting its pointer to null achieves both: its destructor frees nothing, and a later assignment overwrites cleanly. Reading its value, however, is meaningless. Treat every moved-from variable as empty and never inspect it.
The classic bug is moving from a variable still in use. The code compiles. The object silently empties. The next read returns an empty vector or a null handle three statements later, far from the cause.
noexcept decides whether vectors move or copy
Growing a vector relocates every live element into a larger buffer. Relocation prefers moving: one pointer steal per element. But moving can throw for exotic types, and a throw halfway through relocation would lose data.
The standard library resolves this with a check. If the element's move constructor is marked noexcept, meaning it promises never to throw, the vector moves. Otherwise the vector copies, because copies leave the originals intact on failure.
The performance gap is enormous. Moving a million strings steals a million pointers. Copying them allocates a million buffers and duplicates every character. One missing annotation turns an O(1)-per-element relocation into an O(n)-per-element deep copy inside a single push_back.
Mark every owning move operation noexcept when it cannot throw. Pointer theft never throws, so the annotation is nearly always correct. Standard library types declare it for exactly this reason.
Wrappers must forward the caller's intent unchanged
Factories like make_unique, thread launchers, and constructors take arguments only to pass them along. They must preserve each argument's value category: temporaries stay stealable, persistent objects stay protected.
std::forward performs that preservation. Given a parameter declared as T&& inside a template, it re-casts to whatever the caller originally passed. An lvalue caller keeps lvalue protection. An rvalue caller keeps move permission.
caller passes lvalue -> T deduces to U& -> forward yields U& (no theft)
caller passes rvalue -> T deduces to U -> forward yields U&& (theft allowed)Forgetting the forward and passing the parameter by name always yields an lvalue, because named things persist. The wrapper then copies when the caller authorized a move, or binds when it should transfer. The symptom appears frames away as an unexpected copy or a use-after-move.
Note one asymmetry. T&& inside a template is a forwarding reference: it deduces and binds both categories. MyString&& in a concrete class is a plain rvalue reference: it binds temporaries only. The same two characters behave oppositely, and confusing them makes a wrapper either reject valid arguments or steal from live ones.
Where moves pay and what to read next
Moves pay wherever large values change hands. Returning containers from factories. Growing vectors of strings. Sorting buffers of heavy objects. Storing futures and locks that cannot be copied at all.
The idiom for authors is short. Provide a move constructor and move assignment that steal pointers and null the source. Mark both noexcept. Delete or define copies deliberately. Accept sink parameters by value and move them into place.
The idiom for callers is shorter. Move from objects you are done with. Never touch them after. Forward template arguments instead of naming them.
Continue with RAII and smart pointers for the ownership model moves operate within. Read STL containers and algorithms to see where relocation moves run. Review stack versus heap memory for where the stolen buffers live.