Before C++11, returning a large object by value or stuffing one into a container often meant copying it byte for byte, even when the original was about to be discarded. Move semantics, added in C++11, gives the compiler a way to transfer the contents of an object that's about to die instead of duplicating them. This chapter is the conceptual introduction: what problem moves solve, when they happen automatically, and what std::move actually does. Three chapters that follow drill into the mechanics, the rvalue reference syntax, and the class-level rules.
C++ has always had value semantics. Writing auto b = a; produces a brand-new b independent of a. For an int, that costs a handful of bytes. For an Order that owns a std::vector of line items, it costs every byte that the vector points at, plus a fresh heap allocation, plus a memcpy from the old buffer to the new one.
Consider an order that holds many items.
Order copy = original; calls the compiler-generated copy constructor, which walks each member and copies it. For id_ (an int), that's a 4-byte copy. For items_ (a std::vector<LineItem>), the standard library allocates a new heap buffer big enough for five LineItem values and copies each one in, which in turn copies each std::string (allocating a fresh heap buffer for any non-empty name). Five items isn't much, but the same pattern with 5,000 line items copies 5,000 strings and runs 5,000-plus heap allocations.
Copying a std::vector<T> is O(n) and allocates from the heap. For each T it contains, the cost depends on T's copy constructor. Copying a vector of strings is O(n) in the vector size plus O(total characters) for the string contents.
The pain isn't theoretical. Every time a program returned a large container from a function or passed a fat object into another, the compiler dutifully copied it. Most of those copies were pointless because the source was about to be destroyed anyway. Copies are correct, but for heap-owning types they're expensive, and a huge fraction of the copies a program performs aren't useful because the source will never be read again.
The compiler needs a way to tell which objects can safely have their contents transferred. C++ already had a useful distinction in the language: lvalues versus rvalues. The names come from the old days of "left-hand-side" and "right-hand-side" of an assignment, and the modern definitions have drifted from that origin, but the rough idea still works as a starting point.
An lvalue is something with a name and an address. A local variable, a member of a struct, a slot in a vector, anything that the & operator can be applied to. Lvalues persist; the compiler can't assume the program is done with them.
An rvalue is, loosely, a temporary. The result of Order(1001, items) as an expression. The literal 42. The value returned by a function that returns by value, before it's bound to a name. Rvalues don't have stable addresses; they exist long enough to be used in the surrounding expression and then go away.
&a gives a real address. &(a + 1) is not valid, because a + 1 doesn't live anywhere persistent.
The key idea is that rvalues are expiring. No code can refer to that temporary again after the expression ends, because nothing has a name for it. That makes rvalues safe targets for resource transfer. Lvalues are the opposite: they have names, they might be used again, and emptying them out without warning would be a disaster.
C++11 formalized this by adding rvalue reference types, written T&&, that can only bind to rvalues. The conceptual split is what matters here: lvalues persist and must not be emptied without warning, rvalues are expiring and can be transferred from. Sometimes a named variable is known to be about to die, but the compiler sees it as an lvalue because it has a name. That's the role std::move plays.
std::move Is a Cast, Not a MoveThis is the part that often confuses people meeting move semantics for the first time. std::move does not move anything. It doesn't touch memory, it doesn't free a heap buffer, it doesn't zero out a struct. All it does is a compile-time cast that tells the compiler "treat this expression as an rvalue from here on out, even though it has a name". That cast is what enables overload resolution to pick a move constructor or move assignment operator instead of a copy.
What happened: std::move(source) is a cast that rebrands the expression source (normally an lvalue) as an rvalue. The constructor of destination sees an rvalue std::string on the right-hand side, so it picks the move constructor of std::string. The move constructor copies source's internal pointer and size into destination and then resets source's pointer to null. destination now points at the heap buffer that used to belong to source. No memcpy, no allocation, just a pointer swap. Replacing std::move(source) with plain source would cause the compiler to pick the copy constructor instead, allocate a fresh buffer, and memcpy the characters across.
std::move is a request, not an action. It only matters because some other constructor or assignment operator on the receiving end knows how to perform the actual transfer when it sees an rvalue.
int doesn't own anything on the heap. There's nothing to transfer. The "move" is a copy of four bytes, and counter keeps its value because there was no resource to hand off. std::move did its job (the cast), but the underlying type had no move-specific behavior, so the result is identical to a copy. This is why move semantics matter for heap-owning types like std::vector, std::string, and std::unique_ptr, but not for int, double, or trivial structs of those.
std::move itself is free at runtime. It's a compile-time cast. The savings come from what the constructor or assignment operator on the other side does with the rvalue.
Move semantics shows up at the class level as two new special member functions: the move constructor and the move assignment operator. They look like copy operations except their parameter is an rvalue reference (T&&) instead of a const lvalue reference (const T&).
For now, the relevant part is the behavior, not the syntax.
A copy constructor walks the source and duplicates every owned resource. A move constructor transfers every owned resource and then leaves the source in a known empty state. A typical move constructor for a heap-owning class copies the source's pointers and counters into the new object, then resets the source's pointers to null so its destructor does nothing harmful. A sketch:
The move constructor doesn't allocate. It pulls the existing buffer pointer out of other and assigns it to the new object. The noexcept matters too: standard containers like std::vector need to know whether a type's move constructor can throw, and they take a fast path only when the answer is no. The move assignment operator is the same idea applied to an already-constructed left-hand side: it releases whatever resources the left-hand side currently owns, then takes the right-hand side's resources.
The picture: copy = duplicate, move = transfer.
The diagram traces what happens to memory in three snapshots. On the left, Order A owns a heap buffer of five line items. The middle column shows what a copy does: B gets its own brand-new heap buffer, the original buffer is left intact for A, and there are now two buffers in the program. The right column shows a move: B takes ownership of the original buffer, A's pointer is reset to null, and no new buffer ever existed. The destructor of A will eventually run and do nothing because there's nothing to free.
After a move, the source object isn't gone. It's still alive, its destructor will eventually run, and methods can still be called on it. What cannot be assumed is what's inside it. The standard's wording is "valid but unspecified state".
Valid means the object is well-formed enough that operations on it have defined behavior. It can be destroyed, assigned a new value, and queried with operations like size() or empty(). The class invariants still hold; nothing has been corrupted.
Unspecified means no particular contents can be assumed. After std::vector<int> v2 = std::move(v1);, the standard library is allowed to leave v1 empty, or with one stray element, or with the old contents still hanging around. The contents are unknown, and the program must not rely on them.
The first line confirms that snapshot got the contents. The second line probably says 0, but the standard doesn't promise that; it only promises that whatever value it has makes the next line safe. The third line shows the supported way to use a moved-from object: assign a new value to it, then it's back to being a normal vector that can be used freely.
The moved-from object still exists and its destructor still runs at the usual time. Moving doesn't make the object disappear, it empties it. Don't pay for std::move and then leave the source variable alive in a long-lived scope; let it go out of scope soon after.
The rule: after moving from something, treat it as garbage. Either reassign it before reading from it, or let it go out of scope. Standard library types document their moved-from behavior more strictly than user types must, and most leave the source empty. For user-defined types, the author chooses what the moved-from state looks like; it must be safe to destroy and safe to assign to.
std::move isn't required for moves to happen. The compiler inserts them in several common situations, and a good chunk of the performance win from move semantics comes from these automatic cases.
Returning a local variable by value. When a function returns a named local variable by value, the compiler is required (since C++11) to treat that variable as an rvalue for the purpose of the return. So if the type has a move constructor, the return is a move, not a copy.
cart is a local variable. Without C++11, return cart; would normally invoke the copy constructor. With move semantics, the compiler treats cart as an rvalue at the return statement and picks the move constructor instead. In practice, most compilers go further and apply Named Return Value Optimization (NRVO) or copy elision: they construct cart directly in the storage myCart will use, so neither a copy nor a move actually runs. When NRVO can't kick in (for example, when the function might return one of several different local variables), the move constructor is the fallback.
Passing a prvalue (pure rvalue) into a function. When the argument expression is a temporary, the compiler binds it to an rvalue reference parameter if one is available.
The temporary vector built by std::vector<std::string>{"A", "B", "C"} is an rvalue. The items parameter of processOrder is constructed from that rvalue, which means the move constructor of std::vector runs (or copy elision strips the construction entirely). No deep copy of the three strings, and no allocation of a new buffer for them.
Inserting a temporary into a container. When you push_back or emplace_back a temporary into a std::vector, the container moves the temporary into its storage.
Each string literal builds a std::string temporary, which push_back then moves into the vector's internal buffer. Writing std::string s = "order placed"; log.push_back(s); would produce a copy instead, because s is an lvalue. To force a move there, write log.push_back(std::move(s));.
push_back(rvalue) is typically one move construction. push_back(lvalue) is one copy construction. For string-typed elements, that's the difference between two pointer copies and a fresh heap allocation plus a memcpy of the characters.
The decision tree summarizes how the compiler picks between move and copy. If the source is a prvalue (a temporary with no name), the compiler picks the move constructor when one exists. If the source has a name, the move constructor is only chosen with an explicit std::move. If the type doesn't have a move constructor at all, copy is the only option. Move semantics doesn't speed up old code that defined only a copy constructor; opt in by using a type that provides moves, or write your own.
A small benchmark. The next program builds two orders, each carrying ten thousand line items. One run copies the order into a destination; the other moves it. Both ways produce the same result, but the cost differs by orders of magnitude.
The copy spends over a millisecond walking 10,000 line items, allocating a fresh string per item, and copying every character of every name. The move spends a single microsecond copying the vector's internal pointer, size, and capacity into the new vector and nulling out the old one. Three orders of magnitude difference, on the same input, for the same end state.
The takeaway isn't that moves are fast; copy cost scales with the size of the data, while move cost is fixed regardless of size. A vector of int shows almost no gap between copy and move because copying ints is already trivial. Move semantics is most valuable for containers and heap-owning types, and least valuable for types that don't own anything on the heap.
Not every class gets a real win from move semantics. The benefit comes from types that own resources on the heap, where transferring ownership means transferring a pointer instead of duplicating bytes.
Big winners:
std::vector<T>, std::string, std::map, std::unordered_map, and other dynamically-sized containers. Copy cost is O(n); move cost is O(1).std::unique_ptr<T> and std::shared_ptr<T>. unique_ptr is move-only; shared_ptr is copyable, but moves avoid the atomic reference count bump.Negligible benefit:
int, double, bool, and other built-in scalars. There's no resource to transfer.struct Point { int x; int y; };. The bytes get copied either way.std::array<int, N> and similar fixed-size, stack-stored containers. The bytes live inside the object itself.std::pair<int, double> and other compositions of trivially-copyable types.If the object owns something on the heap, moving is cheaper. If the contents live inside the object itself, moving and copying cost the same.
| Type | Sizeof object | Move cost | Copy cost |
|---|---|---|---|
int | 4 bytes | trivial copy | trivial copy |
std::array<int, 1000> | 4000 bytes | memcpy 4000 bytes | memcpy 4000 bytes |
std::vector<int> with 1000 ints | ~24 bytes object + 4000 on heap | swap 24 bytes | allocate + copy 4000 bytes |
std::string with 100 chars | ~32 bytes object + 100 on heap (or SSO) | swap 32 bytes (often) | allocate + copy 100 chars |
std::unique_ptr<Order> | 8 bytes | swap 8 bytes | not copyable |
Order with vector of 10K items | ~24 bytes object + ~hundreds of KB on heap | swap 24 bytes | allocate + copy all items |
The numbers in the "object" column are roughly the size of the type itself, regardless of what it points to. Move cost tracks that, not what's behind the pointer. Copy cost also duplicates everything the object refers to, which for heap-owning types is where the asymmetry comes from. When designing a class, the question "should this support moves?" is mostly "does this class own anything on the heap?" If yes, write or accept a move constructor and a move assignment operator. If no, the compiler-generated copy is already as fast as a move could be.
This chapter has been deliberately light on syntax. The goal was the idea: copies of heap-owning objects are expensive, rvalues are safe to transfer from, std::move is a cast that opts in, and the compiler will move automatically in several common situations. Later chapters extend this picture by digging into the type-system rules behind T&&, the std::forward mechanism templates use to pass arguments through without losing rvalue-ness, and the special member functions a resource-owning class needs to write by hand.
The practical recommendations are short.
const T& for read-only use, by value when ownership transfer is intended.std::move on a named lvalue when ownership should be handed off cheaply.That's enough to use move semantics correctly in most code. The next three lessons explain the rest.
10 quizzes