C++ Move Semantics
Move semantics is one of the most important features introduced in C++11. It lets the compiler transfer ownership of a resource (like heap memory, a file handle, or a socket) from one object to another instead of making an expensive copy. Before C++11, every time you passed a large object by value or returned one from a function, the compiler had to duplicate all of its internal data. Move semantics fixes this by recognizing when an object is about to be destroyed anyway, and simply "stealing" its internals instead of copying them.
Overview / How it works
To understand move semantics, you first need to understand value categories. Every expression in C++ is either an lvalue (has a persistent identity, like a named variable) or an rvalue (a temporary, like the result of an expression or a literal). Historically, C++ only had one kind of reference, T&, which binds only to lvalues. C++11 added a second kind, the rvalue reference, written T&&, which binds specifically to temporaries and other objects that are about to disappear.
This distinction lets classes define two versions of operations like the copy constructor: one that takes const T& (the traditional deep copy), and one that takes T&& (a "move" that pilfers the source object’s internal pointers and leaves the source in a valid-but-empty state). Because the compiler knows an rvalue is temporary, it is safe to gut it — nobody else will use that temporary again.
Internally, a "move" usually means: copy a pointer (or a few pointers/handles) from the source object into the destination object, then null out the source’s pointer so its destructor doesn’t free memory the new object now owns. This is why moves are typically O(1) — independent of how much data the object manages — while copies are O(n), proportional to the amount of data.
Crucially, std::move does not move anything by itself. It is just a cast: it converts an lvalue into an rvalue reference, telling the compiler "treat this object as if it were a temporary, so you’re allowed to call the move constructor/assignment on it." After std::move(x) is used to construct or assign into another object, x is left in a valid but unspecified state — you should not read its value again until you reassign it, though you can safely destroy it or call methods with no preconditions on it (like .clear()).
Syntax
// Rvalue reference parameter
void f(T&& value);
// Move constructor
ClassName(ClassName&& other) noexcept;
// Move assignment operator
ClassName& operator=(ClassName&& other) noexcept;
// Casting an lvalue to an rvalue reference
#include <utility>
T moved = std::move(original);
T&&— an rvalue reference type; binds to temporaries and to anything explicitly cast withstd::move.std::move(x)— defined inutility; performsstatic_cast<T&&>(x), turning an lvalue into an rvalue reference. It does not move anything itself.- Move constructor — a constructor overload taking
ClassName&&; should transfer ownership and leave the source empty. - Move assignment operator — an
operator=overload takingClassName&&; should release its own resources, steal the source’s, and leave the source empty. noexcept— move operations should be markednoexceptwhenever possible, because standard containers likevectoronly use moves during reallocation if they are guaranteed not to throw (otherwise they fall back to copying, for exception-safety reasons).
Examples
Example 1: Moving a std::string
#include <iostream>
#include <string>
#include <utility>
int main() {
std::string a = "Hello, Move Semantics!";
std::string b = std::move(a);
std::cout << "b = " << b << std::endl;
std::cout << "a is now: \"" << a << "\" (length " << a.size() << ")" << std::endl;
return 0;
}
Output:
b = Hello, Move Semantics!
a is now: "" (length 0)
std::string‘s move constructor simply copies the internal heap pointer, size, and capacity from a into b, then resets a to represent an empty string. No characters are copied. Note this exact empty-after-move behavior is common (and guaranteed empty for libstdc++/libc++’s long strings) but the standard only requires a be left in some valid state — don’t rely on it being empty for arbitrary types.
Example 2: A class with its own move constructor and move assignment
#include <iostream>
#include <utility>
class Buffer {
public:
Buffer(size_t size) : size_(size), data_(new int[size]) {
std::cout << "Constructed buffer of size " << size_ << std::endl;
for (size_t i = 0; i < size_; ++i) data_[i] = static_cast<int>(i);
}
// Move constructor: steal the pointer, null out the source
Buffer(Buffer&& other) noexcept : size_(other.size_), data_(other.data_) {
other.size_ = 0;
other.data_ = nullptr;
std::cout << "Move-constructed buffer" << std::endl;
}
// Move assignment operator
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
std::cout << "Move-assigned buffer" << std::endl;
}
return *this;
}
Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
~Buffer() {
delete[] data_;
}
size_t size() const { return size_; }
private:
size_t size_;
int* data_;
};
int main() {
Buffer b1(5);
Buffer b2(std::move(b1));
std::cout << "b1 size after move: " << b1.size() << std::endl;
std::cout << "b2 size after move: " << b2.size() << std::endl;
Buffer b3(2);
b3 = std::move(b2);
std::cout << "b3 size after move-assign: " << b3.size() << std::endl;
return 0;
}
Output:
Constructed buffer of size 5
Move-constructed buffer
b1 size after move: 0
b2 size after move: 5
Constructed buffer of size 2
Move-assigned buffer
b3 size after move-assign: 5
Here Buffer owns a raw int*. The move constructor copies the pointer and size from other into the new object, then sets other‘s pointer to nullptr so its destructor won’t free memory that the new object now owns. The copy constructor and copy assignment are deleted so the class is move-only, which is a common and useful pattern for exclusive-ownership resource wrappers.
Example 3: Watching copies vs. moves inside std::vector
#include <iostream>
#include <vector>
#include <utility>
class Tracker {
public:
Tracker() { std::cout << "Default constructed" << std::endl; }
Tracker(const Tracker&) {
std::cout << "Copy constructed" << std::endl;
}
Tracker(Tracker&&) noexcept {
std::cout << "Move constructed" << std::endl;
}
Tracker& operator=(const Tracker&) {
std::cout << "Copy assigned" << std::endl;
return *this;
}
Tracker& operator=(Tracker&&) noexcept {
std::cout << "Move assigned" << std::endl;
return *this;
}
};
int main() {
std::vector<Tracker> v;
v.reserve(2);
Tracker t;
std::cout << "-- pushing by copy --" << std::endl;
v.push_back(t);
std::cout << "-- pushing by move --" << std::endl;
v.push_back(std::move(t));
return 0;
}
Output:
Default constructed
-- pushing by copy --
Copy constructed
-- pushing by move --
Move constructed
Passing t directly to push_back calls the copy constructor, because t is an lvalue — the vector must assume you still need t afterward. Wrapping it in std::move(t) tells the vector it’s safe to move from t instead, calling the (much cheaper) move constructor. The reserve(2) call avoids reallocation so the output isn’t muddied by extra moves during growth.
Under the hood
When the compiler sees an expression like Buffer b2(std::move(b1)), here is what happens step by step:
std::move(b1)performs a compile-time cast ofb1(an lvalue) to typeBuffer&&. No code runs at this point — it’s purely a type-system trick that changes overload resolution.- Overload resolution now prefers
Buffer(Buffer&& other)overBuffer(const Buffer& other), because an rvalue reference is an exact match for an rvalue-typed expression. - Inside the move constructor, the member initializer list copies the pointer value and size from
other(which is reallyb1) into the new object’s members. This is a shallow, pointer-only copy — the underlying heap array is not touched. - The move constructor’s body then nulls out
other.data_and zeroesother.size_, severingb1‘s ownership. - When
b1eventually goes out of scope, its destructor runsdelete[] data_on anullptr, which is a guaranteed no-op — so the memory is freed exactly once, by whichever object currently owns it.
This is also why the "Rule of Five" exists: if a class manages a resource and needs a custom destructor, it typically also needs a custom copy constructor, copy assignment operator, move constructor, and move assignment operator, so ownership is handled consistently in every scenario.
Common Mistakes
Mistake 1: Using an object after moving from it, expecting its old value
std::string name = "Alice";
std::string backup = std::move(name);
processOrder(name); // BUG: name may now be empty, not "Alice"
Once you write std::move(name), you are telling the compiler you no longer need name‘s value. Reading it afterward as if nothing happened is a logic bug, even though it’s not undefined behavior for standard types. Fix it by not touching the moved-from object again until you reassign it, or by reordering the code so the move happens last:
std::string name = "Alice";
processOrder(name);
std::string backup = std::move(name); // move only after we're done with name
Mistake 2: Forgetting that std::move on a const object does nothing useful
const std::string label = "Report";
std::string copy = std::move(label); // still calls the COPY constructor
Because label is const, std::move(label) produces a const std::string&&, and there is no move constructor overload that accepts a const rvalue reference (move constructors need to mutate the source). Overload resolution silently falls back to the copy constructor, which still compiles — but you get a full copy with no warning that the "move" didn’t do what you expected. The fix is to not declare the source const if you intend to move from it later.
Mistake 3: Not marking move operations noexcept
class Widget {
public:
Widget(Widget&& other) { /* ... */ } // missing noexcept
};
Without noexcept, containers like std::vector cannot safely assume your move constructor won’t throw partway through a reallocation, so during growth they will copy elements instead of moving them — silently defeating the whole point of writing a move constructor. Always add noexcept when your move operations genuinely cannot throw (which is almost always, since they only shuffle pointers).
Best Practices
- Mark move constructors and move assignment operators
noexceptwhenever they truly can’t throw, so standard containers will actually use them. - Always leave the moved-from object in a valid, destructible state — typically an "empty" state with null pointers and zero sizes.
- Never read the value of an object after moving from it; only reassign it or let it be destroyed.
- Use
std::moveonly on objects you are truly done with (usually right before the object goes out of scope or is reassigned). - Prefer accepting parameters by value plus an internal
std::movewhen a function will store its own copy anyway — this lets callers choose to move in an argument for free, while callers passing an lvalue still just pay for one copy. - Don’t call
std::moveon a function’s return value (return std::move(localVar);) — it actually defeats Return Value Optimization (RVO) in many cases and is unnecessary, since returning a local by value is already treated as an rvalue. - If you write a custom move constructor or destructor, follow the Rule of Five and define (or explicitly default/delete) all five special member functions together.
Practice Exercises
- Write a class
IntArraythat owns a dynamically allocatedint*and a size. Implement a move constructor and move assignment operator, then write amain()that demonstrates moving oneIntArrayinto another and prints the size of both before and after the move. - Given
std::vector<std::string> names;already containing several long strings, write code that moves each string out of the vector one at a time into individual variables, printing each string after moving it out. - Predict the output of a small program where a class’s copy and move constructors both print a message, and you call a function that takes the class by value once with an lvalue argument and once with a
std::move‘d argument. Then compile and check your prediction.
Summary
- Move semantics let objects transfer ownership of resources instead of deep-copying them, turning O(n) copies into O(1) pointer swaps.
- Rvalue references (
T&&) bind to temporaries and to anything cast withstd::move. std::movedoesn’t move anything itself — it’s just a cast that enables overload resolution to pick the move constructor/assignment.- A moved-from object is left valid but unspecified; don’t read its value again until you reassign it.
- Mark move operations
noexceptso containers likestd::vectorwill actually use them during reallocation. - Follow the Rule of Five: if you need a custom destructor, define copy/move constructors and assignment operators consistently.
