C++ Module 12 – Move semantics and smart pointers
Writing move constructors and move assignment
Write a C++ move constructor and move assignment operator that take over a resource, keep them noexcept, and leave the moved-from object usable.
What you will learn
- Implement a move constructor and a move assignment operator that take over a resource
- Explain when a growing std::vector moves its elements and when it copies them
- Use a moved-from object safely by destroying it or giving it a new value
- Detect a move that silently became a copy or that breaks on self-move
Before you start
What you’ll learn · Before you start
What you will learn
- Implement a move constructor and a move assignment operator that take over a resource
- Explain when a growing std::vector moves its elements and when it copies them
- Use a moved-from object safely by destroying it or giving it a new value
- Detect a move that silently became a copy or that breaks on self-move
Before you start
On this page
Copying a String of 28 characters allocates a new array of 29 chars and copies every one; moving it copies two
members and allocates nothing, whatever the length. A move constructor: A constructor T(T&& other) that builds a new object by taking over the resources of other, an rvalue the caller has finished with, instead of copying them. It leaves other valid, usually empty, because other's destructor still runs.,
T(T&& other) noexcept, builds a new object by taking over the resources of other, an object the caller has
finished with. A move assignment operator: The member T& operator=(T&& other) that gives an existing object the resources of other: it releases what the object already holds, takes over what other holds and leaves other valid. It must also leave a = std::move(a) harmless. operator, T& operator=(T&& other) noexcept,
does the same for an object that already exists: it releases what it holds, then takes over what other holds. Both
must leave other valid, usually empty, because its destructor still runs, and both should be
noexcept: A specifier that promises a function lets no exception out, as in Buffer(Buffer&&) noexcept. If one tries to leave such a function, std::terminate ends the program. A destructor is noexcept unless it says otherwise. More in “Destructors and RAII in C++”, because a growing std::vector copies, where it can, every element whose move
constructor might throw.
Writing move constructors by hand belongs in the few classes that own a resource directly, such as a raw array or a file handle; every other class gets correct moves from its members.
A move constructor takes the array and leaves the source empty
String owns a heap array of characters, so the compiler’s member-by-member copy would share one array between two
objects; it needs all five special member function: One of the member functions the compiler can write for a class by itself: the default constructor, the destructor, the copy constructor, copy assignment, the move constructor and move assignment. The compiler's versions work member by member. More in “The rule of zero, three and five in C++”. Each of them prints its
name:
// String owns a heap array of chars. Its five special member functions print their names, so the run shows which
// one each line of main calls.
#include <cstddef>
#include <cstring>
#include <print>
#include <utility>
class String {
public:
String(const char* text) : size_{std::strlen(text)}, data_{new char[size_ + 1]} {
std::memcpy(data_, text, size_ + 1);
}
~String() { delete[] data_; } // delete[] on a null pointer does nothing
String(const String& other) : size_{other.size_}, data_{new char[size_ + 1]} {
std::memcpy(data_, other.data_, size_ + 1);
std::println(" copy constructor: a new array of {} chars", size_ + 1);
}
String(String&& other) noexcept
: size_{std::exchange(other.size_, 0)}, data_{std::exchange(other.data_, nullptr)} {
std::println(" move constructor: took the array, left the source empty");
}
String& operator=(const String& other) {
String copy{other}; // the only step that can throw, before *this changes
swap(copy); // copy now holds the old array and frees it
std::println(" copy assignment");
return *this;
}
String& operator=(String&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = std::exchange(other.data_, nullptr);
size_ = std::exchange(other.size_, 0);
}
std::println(" move assignment: freed the old array, took the source's");
return *this;
}
void swap(String& other) noexcept {
std::swap(size_, other.size_);
std::swap(data_, other.data_);
}
const char* c_str() const { return data_ ? data_ : ""; }
std::size_t size() const { return size_; }
private:
std::size_t size_;
char* data_;
};
int main() {
String a = "a string long enough to show";
std::println("String b = a;");
String b = a;
std::println("String c = std::move(a);");
String c = std::move(a);
std::println("b = std::move(c);");
b = std::move(c);
std::println("a = String{{\"reused\"}};");
a = String{"reused"};
std::println("a = \"{}\", b = \"{}\", c.size() = {}", a.c_str(), b.c_str(), c.size());
} from session import run
# Built with AddressSanitizer as well, which would stop the run at a double delete[] or a read of freed memory.
run("""
clang++ -std=c++23 -Wall -Wextra -fsanitize=address string_trace.cpp -o string_trace && ./string_trace
g++-16 -std=c++23 -Wall -Wextra -fsanitize=address string_trace.cpp -o string_trace-gcc
./string_trace > clang.txt && ./string_trace-gcc > gcc.txt && diff clang.txt gcc.txt && echo "GCC prints the same"
""") Output
$ clang++ -std=c++23 -Wall -Wextra -fsanitize=address string_trace.cpp -o string_trace && ./string_trace
String b = a;
copy constructor: a new array of 29 chars
String c = std::move(a);
move constructor: took the array, left the source empty
b = std::move(c);
move assignment: freed the old array, took the source's
a = String{"reused"};
move assignment: freed the old array, took the source's
a = "reused", b = "a string long enough to show", c.size() = 0
$ g++-16 -std=c++23 -Wall -Wextra -fsanitize=address string_trace.cpp -o string_trace-gcc
$ ./string_trace > clang.txt && ./string_trace-gcc > gcc.txt && diff clang.txt gcc.txt && echo "GCC prints the same"
GCC prints the same
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 string_trace_session.py
Step through main and watch the arrays change hands:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- String a = …. a is built from a text of 28 characters, so its constructor allocates an array of 29 chars on the heap: the characters and the terminating zero.
- String b = a;. a is an lvalue, so the copy constructor runs. It allocates a second array of 29 chars and copies every character into it; the run printed copy constructor: a new array of 29 chars.
- String c = std::move(a);. std::move(a) is an xvalue, so the move constructor runs. std::exchange hands a's pointer and size to c and leaves a with nullptr and 0: two members copied, nothing allocated.
- b = std::move(c);. b already owns array 2, so the move assignment frees it first, then takes c's pointer and size. Without that delete[], array 2 would leak.
- String{"reused"}. The right-hand side is a temporary String with an array of its own, 7 chars for the 6 letters of reused and the zero.
- a = String{"reused"};. a was moved from, and it accepts a new value by move assignment like any String: delete[] on its null pointer does nothing, and it takes the array of the temporary, which then ends with the statement.
- The end of main. The run printed a = "reused", b = "a string long enough to show", c.size() = 0. Each String then deletes what it owns, and c deletes a null pointer, which does nothing. AddressSanitizer found no error.
The diagram freezes step 3, the move constructor:
What the move constructor does to the two objects
Text description of the diagram
The diagram is titled "String c = std::move(a);" and has two parts.
- Before: the object a has size_ = 28 and a data_ pointer that points to a heap array of 29 characters.
- After the move constructor: the object c has size_ = 28 and a data_ pointer to the same heap array, whose 29 characters were not copied. The object a now has size_ = 0 and data_ = nullptr; it is empty but valid, because its destructor runs delete[] on nullptr, which does nothing.
A line at the bottom says that the move copies two members and allocates nothing.
The move constructor copies two members and allocates nothing. std::exchange(x, v), from <utility>, stores v
in x and returns the old value, so each member of other is taken and reset in one expression of the member
initialiser list: c gets the pointer and the size, and a is left with a null pointer and a size of 0. The cost is
the same for a string of 28 characters or of 28 million. Nothing in it can throw, which is why it can promise
noexcept. The source stays a valid String: its destructor deletes a null pointer, which does nothing, and
c.size() reports 0 at the end of the run.
Checkpoint
Check: A move constructor that takes over
Move assignment releases first, then takes over
An existing object already owns an array, so the move assignment operator frees it before taking the one from
other; otherwise every assignment would leak. The line a = String{"reused"}; shows the other half of the contract:
a had been moved from, and it accepts a new value like any other String, because a moved-from object is still a
complete object.
The if (this != &other) test handles self-move: Moving an object into itself, as in a = std::move(a), usually through two names for one object. A move assignment that frees its own resource before taking other's would take back what it has just freed, so it tests for it or is written to need no test., an object moved into itself. Without it,
the first statement would free the array that the next statement takes back. Step through a = std::move(a) with the
test, then without it:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- a = std::move(a). The operator is called on a, and its parameter other refers to a as well: this and &other are the same address.
- With the test. if (this != &other) is false, so the body is skipped. a keeps its array and its 28 characters, and the operator returns *this.
- Without it: delete[]. Without the test, the first statement runs delete[] data_ and frees a's array. other.data_ is the same member, so it now holds the freed address too.
- The pointer comes back. data_ = std::exchange(other.data_, nullptr); stores nullptr in other.data_, which is data_, and then assigns the old value back: a holds the freed address again.
- The second delete[]. When a is destroyed, its destructor runs delete[] on the same address a second time: a double free, which is undefined behaviour. The test at the top of the operator is what prevents it.
There are three common ways to write a self-move-safe move assignment:
- A self-check, as here, skips all the work when
otheris the object itself. - Move-and-swap moves
otherinto a temporary and swaps with it, so self-move needs no test at all. - Swap-only exchanges the two objects’ contents and lets
otherfree the old array whenever it is destroyed.
The copy assignment operator uses copy-and-swap: A way to write copy assignment: make a complete copy of the source, swap its members with this object's, and let the copy's destructor free the old resource. It needs no test for self-assignment and changes nothing if the copy throws. More in “The rule of zero, three and five in C++”, as in the rule-of-three lesson, so a
failed allocation leaves *this untouched.
Key idea
Move assignment frees what the object holds, then takes what other holds, and a = std::move(a) must leave the
object as it was.
Checkpoint
Check: Move assignment and self-move
A moved-from object can be destroyed, assigned and asked, not read
Rule C.64 of the C++ Core Guidelines asks a move to leave its source in a valid state: its invariants hold, so it can
be destroyed, assigned a new value, and passed to any function that has no precondition about its value, such as
size() or clear(). Reading its old value is the bug, a use after move: Reading a variable after std::move has handed its contents to another object. The moved-from object is still valid, but its value is not what it was: in this track's example, an empty string and an empty vector. More in “clang-tidy, clang-format and static analysis”: the code
compiles, and what it reads depends on the type and the library. Step through what the run did with its moved-from
strings:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Moved from. After b = std::move(c);, c holds a null pointer and a size of 0. It is still a complete String, in scope, and its destructor will run at the end of main.
- Destroy it. Destroying a moved-from object is always allowed: String's destructor runs delete[] on a null pointer, which does nothing.
- Give it a new value. Assigning is allowed too: a, moved from by String c = std::move(a);, then took the value of String{"reused"} and printed reused.
- Ask it. A function with no precondition about the value works: c.size() printed 0. For library types, size(), empty() and clear() are of this kind.
- Not its old value. Reading the old value is the bug. A library object is only valid with an unspecified value, so what such a read finds depends on the library. String leaves an empty object, a state its documentation can promise.
- Types that say more. A few standard types promise a state: a moved-from std::unique_ptr is null, and a moved-from std::optional still holds a value, the one that was moved from.
For standard library types, the standard promises a
valid but unspecified state: What the standard promises for a moved-from library object unless it says more: its invariants hold, so it can be destroyed, assigned or asked for its size, but its value is not specified and must not be relied on. unless it says more. A few types do say
more: a moved-from std::unique_ptr is null, and a moved-from std::optional still holds a value, the moved-from
one. For your own types, state the moved-from state in the class’s documentation; “empty”, as String does it, is
the easiest promise to keep and to test.
Checkpoint
Check: What a moved-from object may do
Why a growing vector needs noexcept moves
When a std::vector runs out of room, it allocates a bigger array and transfers every element into it. If a move threw
halfway, the old array would already be partly emptied, and push_back could no longer promise to leave the vector as
it was. So the vector transfers each element with std::move_if_noexcept, which moves when the move constructor cannot
throw, or when the type cannot be copied at all, and copies otherwise. Step through one growth:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- No room left. push_back is called on a vector of String whose array is full, so every element has to go to a new, bigger array.
- A bigger array. The vector allocates the new array first. Until the transfer has finished, the old array still holds every element.
- Moves that cannot throw. String's move constructor is noexcept, so std::move_if_noexcept gives an rvalue and each element is moved: two members, no allocation.
- Moves that might throw. If the move constructor might throw and the type can be copied, std::move_if_noexcept gives a const reference, and each element is copied instead. A failed copy then leaves the old array whole, so push_back can leave the vector as it was.
- Types that cannot be copied. A type that cannot be copied is moved even when its move might throw, because there is nothing else to do. A failure halfway would leave the old array partly emptied.
- Free the old array. Once every element has arrived, the vector destroys the old ones and frees their array. That is why rule C.66 of the Core Guidelines says: make move operations noexcept.
Rule C.66 of the Core Guidelines follows from it: make move operations noexcept. A move constructor that the
compiler writes is noexcept exactly when the moves of all the members are. One that you write is noexcept only if
you say so. In a template, the honest promise is a conditional noexcept: A noexcept specifier with a condition, such as noexcept(std::is_nothrow_move_constructible_v<T>), so that a template promises not to throw exactly when the type it wraps makes the same promise., such as
noexcept(std::is_nothrow_move_constructible_v<T>), which promises exactly what T promises.
Checkpoint
Check: Why moves are noexcept
Practise: four rules of a move
Questions on the four ideas above, weakest first; one you miss comes back before the end.
Practice that adapts to you
Practise this lesson
13 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
noexcept and std::vector growth, measured on two libraries Pro: opens with a Pro, Premium or Ultimate pass
Whether std::vector moves or copies its elements is decided at compile time from the element type alone, and the
same class can get a different answer from each standard library. This part measures the difference in heap allocations and
compares four ways to fix it.
In this part:
- How std::move_if_noexcept chooses, case by case
- A rule-of-zero class that copies on one library
- Four fixes and what each costs, measured
- Catching the silent copy at build time
- noexcept for templates that write their moves by hand
6 runnable examples (C++, Python) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Self-move and moved-from objects: two failures reproduced Pro: opens with a Pro, Premium or Ultimate pass
A move leaves its source behind, still alive, still in scope and still callable, and that is where move bugs hide. Both failures below are reproduced with Apple clang 21 and GCC 16, then fixed.
In this part:
- A self-move in an unordered erase, caught by AddressSanitizer
- Three ways to make move assignment safe for self-move
- Valid but unspecified, measured on two libraries
- Your turn: a Buffer that a vector grows without copying
6 runnable examples (C header, C++, Python) · about 5 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Interview questions
-
Fresher · Warm-up
What can you still do with an object after it has been moved from?
Anything that does not depend on its old value. A moved-from object is still a complete, valid object: its destructor will run, you can assign it a new value, and you can call member functions that have no preconditions, such as size(), empty() or clear() on a container. What you cannot rely on is its contents. For most standard library types the standard says only "valid but unspecified", so a moved-from string is usually empty but is not promised to be. A few types promise more: a moved-from std::unique_ptr is null, and a moved-from std::optional still holds a value. For your own classes, document the moved-from state, preferably empty, and keep the class invariant true in it.
-
Mid to senior
Filling a std::vector<Job> is clearly slower in the Linux build than on macOS. Job follows the rule of zero and holds a std::string and a std::deque<int>. Explain the difference, how you would confirm it, and which fix you would choose.
-
Fresher to mid
remove_at(v, i) runs v[i] = std::move(v.back()); v.pop_back(); on a vector of a hand-written String class. Removing the last element crashes with a double free under AddressSanitizer, and removing any other element works. Explain the crash and give two fixes, with their trade-offs.
-
Mid to senior
Design the move operations of a FileHandle class that owns a POSIX file descriptor, an int. What state does a moved-from handle have, what may callers still do with it, and how do your tests prove that no descriptor is closed twice or leaked?
Key takeaways
- A move constructor takes over the source’s resources and resets the source, here with
std::exchange; it copies two members and allocates nothing. - A move assignment operator releases what it holds, then takes over, and must survive
a = std::move(a). - A moved-from object can be destroyed, assigned and asked for its size; its old value is gone.
std::vectormoves its elements while it grows only if their move constructor isnoexceptor they cannot be copied.- Write
noexcepton hand-written moves, andnoexcept(std::is_nothrow_move_constructible_v<T>)in templates. - Writing move constructors and move assignment by hand is for classes that own a resource directly; the others get correct moves from their members.
Terms in this lesson
- conditional noexcept
- A noexcept specifier with a condition, such as noexcept(std::is_nothrow_move_constructible_v<T>), so that a template promises not to throw exactly when the type it wraps makes the same promise.
- move assignment operator
- The member T& operator=(T&& other) that gives an existing object the resources of other: it releases what the object already holds, takes over what other holds and leaves other valid. It must also leave a = std::move(a) harmless.
- move constructor
- A constructor T(T&& other) that builds a new object by taking over the resources of other, an rvalue the caller has finished with, instead of copying them. It leaves other valid, usually empty, because other's destructor still runs.
- self-move
- Moving an object into itself, as in a = std::move(a), usually through two names for one object. A move assignment that frees its own resource before taking other's would take back what it has just freed, so it tests for it or is written to need no test.
- valid but unspecified state
- What the standard promises for a moved-from library object unless it says more: its invariants hold, so it can be destroyed, assigned or asked for its size, but its value is not specified and must not be relied on.
References
- Working Draft, Programming Languages: C++ (N4950): [class.copy.ctor], [class.copy.assign], [except.spec], [utility.exchange], [forward], [lib.types.movedfrom], [res.on.exception.handling] (ISO/IEC JTC1/SC22/WG21)
- Working Draft, Programming Languages: C++ (N4950): [vector.modifiers], [deque.overview], [func.wrap.func.con], [optional.ctor] (ISO/IEC JTC1/SC22/WG21)
- C++ Core Guidelines: C.64, a move operation should move and leave its source in a valid state (Standard C++ Foundation)
- C++ Core Guidelines: C.65, make move assignment safe for self-assignment (Standard C++ Foundation)
- C++ Core Guidelines: C.66, make move operations noexcept (Standard C++ Foundation)
- AddressSanitizer (LLVM Project)
- Options to Request or Suppress Warnings (-Wself-move) (GNU Project)
- Diagnostic flags in Clang (-Wmove, -Wself-move) (LLVM Project)
Report a problem with this lesson
Kept only in this browser. Your Learn progress