Move Semantics¶
Some operations in C++ involve transferring ownership of data from one object to another. Move semantics, introduced in C++11, let you do this without copying, which is often dramatically faster.
To see why this matters, you first have to understand what "copying" actually costs.
The cost of a copy¶
Consider a std::string holding the contents of a 10 MB log file:
What is inside a std::string? Three things:
- a pointer to a heap-allocated character array,
- a
size(how many characters are in use), - a
capacity(how many characters the buffer can hold).
The string object itself is small, typically 24 or 32 bytes on a desktop platform. The 10 MB of actual text lives on the heap.
Now copy the string:
C++ must produce a brand-new std::string whose state is independent of the original. That means:
- Allocate a fresh 10 MB buffer on the heap.
memcpyall 10 MB from the source buffer to the new one.- Set the new string's pointer, size, and capacity to match.
For 10 MB this is slow. For a std::vector<Motor> holding a thousand motors, you also call a thousand copy constructors. Whenever the data being copied does not need to persist at the source, this work is wasted.
What move does instead¶
A move transfers ownership of the underlying resource, without copying it.
std::string log = readEntireFile("server.log");
std::string copy = std::move(log); // move, not copy
Now C++ does this:
- Copy the three small fields (pointer, size, capacity) from
logintocopy. - Set
log's pointer tonullptrand its size and capacity to zero, so that its destructor does nothing harmful.
That is it. No 10 MB allocation, no memcpy, no thousand copy constructors. Three pointer-sized writes, regardless of the size of the data.
before move
log ●──► [ 10 MB of text ]
after move
log (empty)
copy ●──► [ 10 MB of text ] ← same buffer, not copied
After the move, copy owns the 10 MB and log is in a valid but unspecified state (usually empty). You may assign to it or destroy it, but you should not assume any particular contents.
When move happens automatically¶
You very rarely have to type std::move yourself. The compiler inserts moves automatically in two important cases:
1. Returning a local object from a function.
std::vector<int> readSamples() {
std::vector<int> samples;
for (int i = 0; i < 1000; ++i) {
samples.push_back(i);
}
return samples; // moved (or even better, see RVO below)
}
std::vector<int> data = readSamples(); // no copy, no move call needed
2. Passing a temporary into a function.
std::vector<std::string> names;
names.push_back(std::string("Alice")); // the temporary string is moved in
names.push_back("Bob"); // same, the temporary is moved
The compiler can see that the source value will not be used afterwards, so it moves rather than copies. In modern C++ this happens by default, and the language-level optimisation called Return Value Optimisation (RVO) often eliminates even the move: the function builds the return value directly in the caller's variable.
Do not write
return std::move(samples);on a local variable. It disables RVO and is actually slower than justreturn samples;.
When to write std::move yourself¶
The pattern is: "I have a named variable, I am done with it, and I want its contents to land somewhere else without a copy."
class Logger {
public:
Logger(std::string filename)
: filename_(std::move(filename)) {} // move the parameter into the member
private:
std::string filename_;
};
The parameter filename is a named local variable, and the compiler will not move it for you automatically. Without std::move, the member is copy-constructed from it (a needless allocation). With std::move, the member adopts the parameter's storage.
Another common case: transferring ownership of a unique_ptr.
std::unique_ptr<Motor> motor = std::make_unique<Motor>(1);
sim.installMotor(std::move(motor));
// motor is now empty; sim owns the Motor
unique_ptr cannot be copied (copying would create a second owner), so std::move is the only way to hand one over.
Move constructors and move assignment¶
When you copy an object, the compiler calls its copy constructor. When you move one, it calls its move constructor. For standard library types (std::string, std::vector, std::unique_ptr, std::map, etc.) both are already implemented correctly.
If you write your own class and follow the Rule of Zero, letting your members manage themselves, the compiler also generates a correct move constructor for free. You almost never have to write one by hand.
Designing a movable class¶
The Rule of Zero covers almost everything. But occasionally a class owns a raw resource that no standard type already wraps — a handle from a C API, a hardware connection, a lock. Then the compiler-generated operations are wrong, and you must write the move operations yourself.
Take the SensorConnection from RAII: it opens a connection in its constructor and closes it in its destructor. A connection is unique — there is one physical link, and copying the object cannot duplicate it. So the right design is move-only: you can transfer the connection out of one object into another, but you cannot copy it. This is exactly how std::unique_ptr behaves.
class SensorConnection {
public:
explicit SensorConnection(int id) : id_(id) {
std::cout << "Opened connection to sensor " << id_ << "\n";
}
~SensorConnection() {
if (id_ != -1) { // a moved-from object owns nothing
std::cout << "Closed connection to sensor " << id_ << "\n";
}
}
SensorConnection(SensorConnection&& other) noexcept // move constructor
: id_(other.id_) {
other.id_ = -1; // leave the source empty
}
SensorConnection& operator=(SensorConnection&& other) noexcept { // move assignment
if (this != &other) { // guard against `x = std::move(x)`
if (id_ != -1) {
std::cout << "Closed connection to sensor " << id_ << "\n"; // release ours first
}
id_ = other.id_; // steal the other's
other.id_ = -1; // leave it empty
}
return *this;
}
SensorConnection(const SensorConnection&) = delete; // no copying
SensorConnection& operator=(const SensorConnection&) = delete;
private:
int id_ = -1; // -1 means "owns no connection"
};
Four things make it correct:
- A way to represent "empty." After being moved from, an object must own nothing, so its destructor does nothing. Here
id_ == -1is that state, and the destructor checks for it. - The move constructor steals. It takes the handle out of
otherand then setsotherto empty — no connection is opened or closed, just two integer writes. - Move assignment releases, then steals. It closes the connection it currently holds before taking the other's, and guards against self-assignment (
x = std::move(x)). - Copying is
= deleted. That states the move-only intent and turns any attempt to copy into a compile error, rather than a silent, broken duplicate.
Mark the move operations noexcept. It promises they cannot throw — true here, since they only shuffle a handle around. This matters in practice: std::vector will only move your objects when it grows (rather than copy them) if their move constructor is noexcept.
This is the Rule of Five: once you write a destructor and the move operations, the compiler stops filling in the rest, so you account for all five — here, by deleting the copies. (The Rule of Zero is how you usually avoid all of this.)
Prefer the Rule of Zero even here. All of this disappears if the resource lives in a
std::unique_ptrmember (with a custom deleter for a C API) or a standard container: the generated moves are correct, copying is disabled for free, and you write none of the five. Hand-write the operations only for a raw resource that nothing else wraps — and keep that wrapper as small as you can.
A note on the moved-from object¶
After std::move(x), x is still a valid object. You can destroy it; you can assign a new value to it. But you should not assume anything about its current value.
std::string a = "Hello";
std::string b = std::move(a);
std::cout << a << "\n"; // legal, but the result is unspecified
a = "Goodbye"; // legal and well-defined
A simple rule of thumb: treat a moved-from variable as if it has been freshly default-constructed. Either assign to it or let it go out of scope.
Summary¶
- A copy duplicates the underlying data; potentially expensive.
- A move transfers ownership of the underlying data; cheap (a few pointer writes).
- The compiler inserts moves automatically for returns and temporaries.
- Write
std::moveyourself when you have a named variable whose contents you want to hand off. - Do not
std::movea return value of a local variable; it disables RVO. - Use moves when transferring
unique_ptrs; they cannot be copied. - Own a raw resource? Make the class move-only —
noexceptmove operations, copies= deleted — or wrap it in aunique_ptrand write none of them (Rule of Zero). - A moved-from object is valid but unspecified; assign to it or destroy it.