A move constructor builds a new object by stealing the resources of an existing one, instead of making a copy. It exists so that objects which own expensive resources (heap memory, file handles, network connections) can be transferred efficiently between functions and containers. This lesson covers what move semantics solve, the rvalue reference syntax &&, the move constructor signature, what "valid but unspecified state" means, when the compiler generates one for you, and how std::move participates in all of this.
Some objects own resources. A std::vector<int> holds a pointer to a heap-allocated array. A std::string holds a pointer to a heap-allocated character buffer. A custom class might hold a raw new-allocated buffer or a file handle. Whatever the resource, copying the object means duplicating the resource.
Consider a Cart class that owns a heap buffer of prices:
Without move semantics, returning local from makeCart would build a copy of a one-million-element heap array, then destroy the original. Two million doubles allocated, copied, and freed, to hand back one. The copy constructor is correct, but it does eight megabytes of work that the caller does not need.
When an object is about to be destroyed anyway (like the local at the return point), the program doesn't need to copy its contents. It can transfer ownership of the heap pointer to the destination, leave the original pointing at nothing, and skip the allocation and copy entirely. That transfer is what a move constructor does.
&& TypeC++11 introduced a new kind of reference written with two ampersands: T&&. This is an rvalue reference. To understand why it exists, you have to understand the lvalue/rvalue distinction.
Every expression in C++ is either an lvalue or an rvalue:
cart is an lvalue.makeCart(5), the literal 42, the expression a + b, all rvalues. They don't have a stable identity, and the program will throw them away shortly.A plain reference T& (an lvalue reference) binds only to lvalues. You can't bind it to a temporary:
A const T& is more flexible: it binds to both, which is why const T& parameters are everywhere in C++. But const rules out modifying the object.
An rvalue reference T&& binds only to rvalues:
T&& gives code a way to say "I want to handle a value that's about to be destroyed, and I want to be allowed to modify it." The "modify" part is what enables stealing its resources.
| Reference Type | Binds to lvalues? | Binds to rvalues? | Can modify? |
|---|---|---|---|
T& | Yes | No | Yes |
const T& | Yes | Yes | No |
T&& | No | Yes | Yes |
The rvalue reference is the type the compiler uses to find a move constructor when one is available, falling back to the copy constructor (which takes const T&) when it isn't.
Quick Check: Which of these compiles?
<details> <summary>Answer</summary>
B. B works because const int& binds to temporaries. C works because int&& binds to rvalues. A fails because a plain int& cannot bind to a temporary. D fails because x is an lvalue and int&& rejects lvalues.
</details>
A move constructor is a constructor whose parameter is an rvalue reference to its own class:
Three parts of that signature matter:
other or rhs (right-hand side).std::vector only use the move constructor (instead of copying) when it is marked noexcept. Without noexcept, a vector that needs to grow will copy elements one by one instead of moving them, which defeats the purpose.A move constructor for the Cart class:
Sample Output:
No "copy ctor" line is printed. With modern compilers, the return value optimization may eliminate even the move (constructing local directly into c's memory), but if the move did happen, no million-element copy would. The move constructor simply takes ownership of other.prices, then resets other to a safe empty state.
Inside a move constructor, the work splits into two parts. First, copy the small handles (the pointer, the size, any other lightweight tracking fields) from the source. Second, leave the source in a state where its destructor will not free the resources you just stole.
The heap buffer doesn't move. Only the pointer changes hands. The source's pointer is reset to nullptr, so when the source is destroyed, delete[] nullptr; runs (which is a safe no-op) instead of freeing the buffer that now belongs to the destination.
Move semantics comes down to pointer reassignment with no deep copy.
If you forget the reset step, both objects hold the same pointer, and the buffer gets freed twice when both objects go out of scope. That is a double-free, and the program will crash or corrupt the heap.
What is wrong with this move constructor?
Both other and the new object now hold the same pointer. When other is destroyed, its destructor calls delete[] prices;. When the new object is destroyed later, its destructor calls delete[] on the same address. Double-free, undefined behavior.
Fix: always reset the source's owning fields to a safe value.
After a move, the moved-from object is still alive. It will eventually be destroyed (or assigned to), so its destructor needs to do something sensible. The C++ standard says a moved-from object must be in a valid but unspecified state.
"Valid" means:
"Unspecified" means:
For the Cart example, after Cart moved = std::move(original);, the moved-from original has size = 0 and prices = nullptr. The destructor runs fine on that state. If somebody calls original.prices[0] afterward, that's their bug, not the move's.
Standard library types follow this rule. After moving from a std::string, the source might be empty or might still hold its old characters; you don't know. After moving from a std::vector, the source is typically (but not required to be) empty. The safe rule: don't read from a moved-from object. Assign a new value to it, or let it go out of scope.
Quick Check: What does this print?
a length: 14a length: 0b: Wireless Mouse, and a length: could be anything<details> <summary>Answer</summary>
C. b is guaranteed to hold "Wireless Mouse" after the move. a is in a valid but unspecified state. Most implementations leave it empty, but the standard does not require this. Reading a is legal (no UB), but you cannot depend on its contents.
</details>
std::move: Casting to an rvalueA named variable is always an lvalue, even if it was declared as T&& x. The rule is deliberate: once you can refer to something by name, it's stable enough to be an lvalue, and binding lvalues to rvalue references would be unsafe.
So this doesn't call the move constructor:
To move from a, the compiler needs an explicit signal to treat a as an rvalue, even though it has a name. std::move provides that signal. It is a cast to an rvalue reference. Despite the name, it does not move anything by itself; it only changes the type so the compiler will select the move constructor at the next step.
After this line, a is in its moved-from state, and b holds the buffer that a used to own.
std::move is equivalent to static_cast<T&&>(x). It is a zero-cost compile-time operation. The actual work happens in the move constructor that gets called as a result.
A common use is to push existing objects into a container without copying:
Sample Output (libstdc++):
The two push_back calls insert the same string, but the first copies it (so the original name is untouched) while the second moves it (so the original name is left empty on most implementations).
When std::move is the wrong choice, the worst case is that you slow yourself down by accidentally suppressing a copy elision optimization. It's not unsafe, but using it on a local variable that's about to be returned is unnecessary and sometimes counterproductive:
The compiler would have done a better job on return local; because that allows return value optimization to skip the move entirely. Use std::move when you mean it, not as a habit.
The compiler generates a default move constructor for your class only under specific conditions. The rules connect to the Rule of Three / Five / Zero, which is a heuristic about when you need to write your own special member functions.
Rule of Three (pre-C++11): If a class needs a custom destructor, copy constructor, or copy assignment operator, it almost certainly needs all three.
Rule of Five (C++11+): Add the move constructor and move assignment operator to the list. If you need any of the five, you probably need all of them.
Rule of Zero (modern C++): Design your class so it owns no raw resources directly. Use std::vector, std::string, std::unique_ptr, etc., as members. Then you don't need to write any of the five, and the compiler-generated versions work correctly.
The compiler will generate a move constructor if all of these hold:
The moment you declare any of those special members yourself, the compiler stops generating the move constructor (and sometimes the move assignment operator). Consider a class like this:
Because ~CartHistory() is declared explicitly, no move constructor or move assignment operator is generated. Moving a CartHistory falls back to the copy constructor with no warning. For a class wrapping a big vector, that's an expensive bug.
The fix is either to remove the unnecessary destructor (the vector already handles its own cleanup), or to explicitly default the move operations:
This is the verbose way. The cleaner approach is to follow the Rule of Zero: don't write a destructor unless the class owns a raw resource, and the compiler will handle moves correctly on its own.
| Rule | When it applies | What to write |
|---|---|---|
| Rule of Zero | Class owns no raw resources | Nothing. Let the compiler generate all five. |
| Rule of Three | Pre-C++11 class with raw ownership | Destructor, copy ctor, copy assign |
| Rule of Five | Modern class with raw ownership | All five: destructor, copy ctor, copy assign, move ctor, move assign |
Quick Check: Will the compiler generate a move constructor for this class?
std::string is not movable<details> <summary>Answer</summary>
B. The user-declared destructor (even an empty one) suppresses the implicit move constructor and move assignment operator. To get them back, either remove the empty destructor or write Order(Order&&) noexcept = default; explicitly. std::string and std::vector are both movable, so the underlying machinery would work fine.
</details>
A full class showing the move and copy constructors side by side, plus a small program demonstrating which one runs in different situations:
Sample Output:
a's line is dtor(0) because a was moved-from: its size is now 0 and its prices is nullptr. The other three destructors run on intact carts of size 3. Each one calls delete[] on a valid heap pointer, and no double-free occurs because only one Cart ever owns each buffer at any time.
The cost saving becomes clear when n is a million instead of three. The copy lines each do a million-double memcpy plus a million-double heap allocation. The move line does none of that. It is the same work as moving a single pointer plus updating two integers.
Q1: What problem do move semantics solve?
Copying an object that owns a heap resource (like a std::vector or std::string) requires duplicating the resource, which is expensive for large objects. Move semantics let a destination object take ownership of the source's resource handle (typically a pointer), leaving the source in a valid but unspecified state. This turns an O(n) copy into an O(1) pointer reassignment. The savings show up everywhere standard library containers grow, shrink, or get returned from functions.
Q2: Why is the move constructor's parameter `T&&` instead of `const T&` or `T&`?
T&& (rvalue reference) is a type that binds only to rvalues, which are temporaries and values about to expire. The move constructor needs to modify the source (to reset its pointer to nullptr, for example), so the parameter cannot be const. It also shouldn't bind to lvalues by default, since stealing a named variable's resources without an explicit cast would surprise the caller. The && overload is what overload resolution picks when the argument is an rvalue and a move constructor is available; lvalues route to the copy constructor instead.
Q3: Why mark the move constructor `noexcept`, and what happens if you don't?
noexcept promises the move will not throw an exception. Standard library containers like std::vector check this promise: if the move constructor is noexcept, they use it during reallocation (e.g., when the vector grows). If it might throw, they fall back to copying for safety, because a half-completed move could leave the container in an inconsistent state. Without noexcept on the move constructor, you lose the performance benefit in exactly the situations where it matters most.
Q4: What does `std::move` do?
std::move does no moving by itself. It's a cast that turns an lvalue expression into an rvalue reference, equivalent to static_cast<T&&>(x). The actual move happens when the resulting rvalue is passed to a constructor or assignment operator that takes T&&, which the compiler then selects via overload resolution. Calling std::move on a value that isn't about to be discarded leaves it in a moved-from state, which is legal but means you shouldn't read from it afterward.
Q5: When does the compiler generate a move constructor automatically, and when does it stop?
The compiler generates an implicit move constructor only if the class declares none of the other special members: copy constructor, copy assignment operator, move assignment operator, or destructor. The moment you declare any one of those, the implicit move constructor (and usually move assignment too) is suppressed. This is the Rule of Five. A class with an explicit destructor (for example, one that logs) loses its move operations without any warning from the compiler. The fix is either to declare all five with = default or, better, to follow the Rule of Zero and use members like std::vector or std::unique_ptr that already handle move semantics correctly.
Exercise 1: Write a class Buffer that owns a heap-allocated int* and an int size. Implement a constructor that takes a size, a destructor that releases the memory, and a move constructor that transfers ownership. Print a message in each of the three so you can see when they run.
Expected Output (sample):
<details> <summary>Solution</summary>
</details>
Exercise 2: What does this program print? Why?
Expected Output:
<details> <summary>Solution</summary>
b is built by the copy constructor, which duplicates a's buffer. c is built by the move constructor, which takes ownership of a's buffer. Both end up holding the same string content, but b has its own allocation while c reused a's. Reading a after the move would be legal but produce an unspecified value.
</details>
Exercise 3: Fix the move constructor in this class so it doesn't double-free the heap memory.
<details> <summary>Solution</summary>
The move constructor copied the pointer but didn't reset the source. Both objects' destructors will try to delete[] the same address.
delete[] nullptr; is a guaranteed no-op, so the moved-from object's destructor now has nothing to free.
</details>
Exercise 4: Will the compiler generate a move constructor for this class? Explain.
<details> <summary>Solution</summary>
No. The explicit (even empty) destructor suppresses the implicit move constructor and move assignment operator. Without them, attempts to move an Order will fall back to the copy operations, which deep-copy the vector.
Two fixes:
Order(Order&&) noexcept = default; and Order& operator=(Order&&) noexcept = default; to bring the implicit move operations back.</details>
Exercise 5: Predict the output of this program. Which constructor runs at each line?
Expected Output (with NRVO):
<details> <summary>Solution</summary>
a is an lvalue.std::move(a) casts to rvalue.t directly into d's memory. Without NRVO, a "move" line would also appear.The exact NRVO behavior depends on the compiler and optimization level. Older or simpler compilers might show default then move.
</details>
Exercise 6: Modify the Buffer class from Exercise 1 to also provide a copy constructor. Then write code that triggers both the move and copy constructors and prints which one ran.
Expected Output:
<details> <summary>Solution</summary>
</details>
Exercise 7: Why is noexcept on the move constructor important when working with std::vector? Write a short experiment that demonstrates the difference.
<details> <summary>Solution</summary>
std::vector only uses a class's move constructor during reallocation if the move is marked noexcept. Otherwise it falls back to copy for exception safety.
The second loop prints "copy!" lines on every reallocation, while the first does not, because vector had no safe move to fall back on.
</details>
Exercise 8: Refactor this class to follow the Rule of Zero. Move semantics should still work.
<details> <summary>Solution</summary>
The class doesn't own any raw resources directly. Its std::vector member already handles its own copy, move, and destruction. Drop all the custom special members:
The compiler generates a correct copy constructor, move constructor, and destructor, all of which forward to the corresponding std::vector operations.
</details>
10 quizzes