That limitation made certain designs inefficient:
- Returning large objects was expensive.
- Containers had to copy elements during reallocation.
- Smart pointer ownership transfer was awkward.
- Performance-critical systems required hacks.
Move semantics changed the language’s core model of value transfer.
It introduced the idea that:
Resources don’t have to be duplicated. They can be transferred.
This single idea reshaped modern C++.
Move semantics exists because C++ distinguishes between different kinds of expressions.
An object can be:
- lvalue → has identity and stable address
- rvalue → temporary, about to die
If an object is temporary, copying it is wasteful.
It will be destroyed anyway.
Example
std::string s = "hello";
std::string t = s; // copy (s is lvalue)
std::string u = "world"; // move (temporary)
Intuition:
lvalue:
+--------+
| s | stable memory
+--------+
rvalue:
temporary created → used → destroyed
Move semantics activates when the compiler sees a temporary.
Lvalue References (`T&`) — The Original Reference
Before C++11, there was only one kind of reference:
T&
This is what people informally call:
“left-side reference”
Because it binds to something that can appear on the left side of assignment.
Example:
int x = 5;
int& ref = x;
Here:
xis an lvaluerefis an lvalue referencerefbecomes an alias ofx
What Is an Lvalue?
An lvalue is something that:
- Has identity
- Has a stable memory address
- Can appear on the left side of assignment
Example:
int a = 10;
a = 20; // valid → lvalue
Memory:
+-----+
| a | ← stable address
+-----+
ref →
|
v
+-----+
| a |
+-----+
Rvalue References — The Enabler
C++11 introduced:
T&&
This binds specifically to rvalues.
Example:
int&& x = 10;
10 is temporary → x binds to it.
But the true power appears inside classes.
The Move Constructor — Stealing Resources
When a temporary object is used to initialize another object, the move constructor is called.
Let’s build a manual resource-owning type.
class Buffer {
int* data;
size_t size;
public:
Buffer(size_t s) : size(s) {
data = new int[size];
}
// Move constructor
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size)
{
other.data = nullptr;
other.size = 0;
}
~Buffer() {
delete[] data;
}
};
Instead of copying memory, we transfer pointer ownership.
Ownership Transfer Diagram
Before move:
other
+------------------+
| data -> [memory] |
| size |
+------------------+
After move:
this
+------------------+
| data -> [memory] |
| size |
+------------------+
other
+------------------+
| data = nullptr |
| size = 0 |
+------------------+
No allocation.
No duplication.
Just pointer reassignment.
That is zero-overhead abstraction.
Move Assignment Operator — Rebinding Ownership
Initialization and assignment are different operations.
Assignment must:
- Release current resource
- Take ownership of new one
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
The object remains valid, but its internal state changes.
Why We Must Reset the Source
If we do not nullify the moved-from object:
Two objects would delete the same memory.
This leads to double free → undefined behavior.
Move semantics requires that:
- The moved-from object remains valid.
- But its state is unspecified.
This is a critical guarantee in modern C++.
Rule of Five — Resource Management Discipline
If your class manages resources manually, you must think about:
- Destructor
- Copy constructor
- Copy assignment
- Move constructor
- Move assignment
This is called the Rule of Five.
Why five?
Because move semantics introduced two additional special member functions.
If you define one, you probably need to define all relevant ones.
Modern C++ philosophy:
If you manage raw memory manually, you accept responsibility for all ownership semantics.
std::move — What It Actually Does
This is one of the most misunderstood functions.
std::move(x)
It does not move anything.
It performs:
static_cast<T&&>(x)
It converts an lvalue into an rvalue reference.
It tells the compiler:
“You may treat this object as temporary.”
Example:
std::string a = "hello";
std::string b = std::move(a);
After this:
bowns the memoryais valid but unspecified
Copy vs Move — Performance View
Copy:
A ---> [memory]
B ---> [new memory copy]
Move:
A ---> nullptr
B ---> [memory]
Copy complexity: O(n)
Move complexity: O(1)
This difference matters in:
- Large containers
- Real-time systems
- Embedded Linux pipelines
- High-frequency systems
For senior roles, this distinction must be intuitive.
What Is `noexcept` in C++
noexcept is a specifier that tells the compiler:
“This function is guaranteed NOT to throw exceptions.”
Basic example:
void f() noexcept {
// this function promises not to throw
}
If an exception escapes this function →
the program calls:
std::terminate();
It does not unwind the stack normally.
Why `noexcept` Exists
Before C++11, we had:
void f() throw();
But dynamic exception specifications were slow and complex.
C++11 replaced them with: noexcept
It is:
- Simpler
- Faster
- Compile-time friendly
- Required for move optimization
What Happens If a `noexcept` Function Throws?
Example:
void f() noexcept {
throw std::runtime_error("error");
}
At runtime:
std::terminate() is called
No stack unwinding.
Immediate termination.
So noexcept is a contract.
Why `noexcept` Is Critical for Move Semantics
This is the most important part.
Consider:
std::vector<MyType> v;
When vector reallocates (grows capacity):
It must move or copy elements.
But what if moving throws?
Vector must preserve strong exception guarantee:
If reallocation fails, original data must remain intact.
So the rule is:
If move constructor is noexcept → use move
If not noexcept → use copy
Example:
class A {
public:
A(A&&) noexcept { }
};
Vector will move A during reallocation.
If you remove noexcept:
A(A&&) { }
Vector may fallback to copying.
This silently destroys performance.