Move Semantics & Perfect Forwarding
Move semantics (C++11) is one of the most important performance features in modern C++. It eliminates unnecessary copies by transferring resources instead of duplicating them.
Table of Contents
1 β The Problem: Unnecessary Copies
2 β Value Categories
3 β Move Constructor & Move Assignment
4 β std::move
5 β Rule of Five & Rule of Zero
6 β noexcept
7 β Perfect Forwarding
8 β Copy/Move Elision (RVO/NRVO)
9 β Summary Table
Glossary β Key Terms at a Glance
| Term | Meaning |
|---|---|
| lvalue | Expression with identity (has an address) β named variables |
| rvalue | Temporary/expiring value β no persistent address |
| Move constructor | Steals resources from an rvalue β O(1) |
std::move |
Cast to rvalue β tells compiler βIβm done with thisβ |
std::forward |
Conditional cast β preserves lvalue/rvalue-ness in templates |
| Universal reference | T&& in template context β binds to both lvalues and rvalues |
| Rule of Five | If you define one of destructor/copy/move, define all five |
| Rule of Zero | Use RAII types so you donβt need to define any of the five |
| RVO/NRVO | Compiler eliminates copy/move when returning by value |
noexcept |
Promise that a function wonβt throw β required for STL optimizations |
1 β The Problem: Unnecessary Copies
1.1 Why Copies Hurt
std::vector<int> createLargeVector() {
std::vector<int> v(1'000'000);
// fill v...
return v; // Pre-C++11: COPIES entire vector to caller!
}
std::vector<int> result = createLargeVector();
// Without move semantics: 1 million ints copied needlessly
β οΈ Problem: Before C++11, returning large objects by value meant deep copying all their data. Move semantics solves this by transferring ownership of the internal resources (pointers, buffers) instead.
2 β Value Categories
2.1 lvalue vs rvalue
Definition: Every expression in C++ is either an lvalue (has identity, has address) or an rvalue (temporary, no persistent address).
int x = 42; // x is an lvalue (has identity, has address)
// 42 is an rvalue (temporary, no address)
int& ref = x; // OK: lvalue ref binds to lvalue
// int& ref = 42; // ERROR: lvalue ref cannot bind to rvalue
int&& rref = 42; // OK: rvalue ref binds to rvalue
// int&& rref = x; // ERROR: rvalue ref cannot bind to lvalue
| Expression | Type | Example |
|---|---|---|
| Named variable | lvalue | x, arr[0], *ptr |
| Temporary | rvalue | 42, x + y, std::string("hi") |
| Function return value | rvalue | getVector() |
std::move(x) |
rvalue (cast) | Converts lvalue to rvalue |
3 β Move Constructor & Move Assignment
3.1 Implementing Move Operations
class Buffer {
int* data;
size_t size;
public:
// Constructor
Buffer(size_t n) : data(new int[n]), size(n) {}
// Destructor
~Buffer() { delete[] data; }
// Copy constructor β EXPENSIVE (deep copy)
Buffer(const Buffer& other) : data(new int[other.size]), size(other.size) {
std::copy(other.data, other.data + size, data);
}
// Copy assignment
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data;
size = other.size;
data = new int[size];
std::copy(other.data, other.data + size, data);
}
return *this;
}
// Move constructor β CHEAP (steal resources)
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // leave source in valid state
other.size = 0;
}
// Move assignment
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
};
π Why This Logic: The move constructor simply copies the pointer and size, then nullifies the source. No allocation, no memcpy. The source is left in a valid-but-empty state.
Copy vs Move Performance:
Copy: allocate new memory β copy all elements β O(n)
Move: swap 2 pointers + 1 size_t β O(1)
For a vector with 1 million elements:
- Copy: ~4MB allocation + memcpy
- Move: 3 pointer/int swaps (~24 bytes)
4 β std::move
4.1 When to Use std::move
Definition: std::move doesnβt move anything. Itβs a cast that says βIβm done with this, you can take its resources.β The actual move happens in the move constructor/assignment.
std::string s1 = "Hello World";
std::string s2 = std::move(s1); // s1's buffer transferred to s2
// s1 is now in a "valid but unspecified state"
// Only safe operations on moved-from objects: assign or destroy
std::cout << s2; // "Hello World"
s1 = "Reused"; // OK: can assign after move
When to use std::move:
// 1. Passing objects you no longer need
std::vector<int> v{1, 2, 3, 4, 5};
processData(std::move(v)); // avoids copy β v is moved into parameter
// 2. Storing in containers
std::vector<std::string> names;
std::string name = "Alice";
names.push_back(std::move(name)); // move instead of copy
// 3. Moving into member variables
class Widget {
std::string name;
public:
Widget(std::string n) : name(std::move(n)) {}
// Parameter n is copied/moved into function, then moved into member
};
4.2 When NOT to Use std::move
β οΈ Common anti-patterns:
// DON'T: move from const objects (no effect β falls back to copy)
const std::string s = "hello";
auto s2 = std::move(s); // COPIES, not moves! const prevents move.
// DON'T: move return values (prevents RVO)
std::string bad() {
std::string s = "hello";
return std::move(s); // WRONG β prevents compiler optimization!
}
std::string good() {
std::string s = "hello";
return s; // RIGHT β compiler applies RVO (even better than move)
}
// DON'T: move trivial types (int, double, pointers)
int x = 42;
int y = std::move(x); // Pointless β ints don't have resources to transfer
5 β Rule of Five & Rule of Zero
5.1 Special Member Functions
Rule of Five: If you define ANY of these, you should define ALL of them:
| Special Member | Signature |
|---|---|
| Destructor | ~T() |
| Copy constructor | T(const T&) |
| Copy assignment | T& operator=(const T&) |
| Move constructor | T(T&&) noexcept |
| Move assignment | T& operator=(T&&) noexcept |
Rule of Zero (PREFERRED): Donβt manage resources manually. Use smart pointers and standard containers:
class Person {
std::string name;
std::unique_ptr<int[]> scores;
std::vector<std::string> friends;
// No need to write any of the 5 special members!
// std::string, unique_ptr, vector all handle their own memory.
};
= default and = delete:
class Widget {
public:
Widget() = default; // compiler-generated default
Widget(const Widget&) = delete; // prevent copying
Widget& operator=(const Widget&) = delete;
Widget(Widget&&) = default; // compiler-generated move
Widget& operator=(Widget&&) = default;
};
6 β noexcept
6.1 Critical for Move Operations
β οΈ Always mark move operations noexcept. Without it, STL containers fall back to copying for exception safety.
class Buffer {
public:
Buffer(Buffer&& other) noexcept; // MUST be noexcept
Buffer& operator=(Buffer&& other) noexcept; // MUST be noexcept
};
// WHY: std::vector::push_back uses move ONLY if move constructor is noexcept
// If not noexcept, it copies (to maintain strong exception guarantee)
7 β Perfect Forwarding
7.1 Universal References & std::forward
Problem: How to write a wrapper that passes arguments along exactly as they were received (preserving lvalue/rvalue-ness)?
The Problem:
template<typename T>
void wrapper(T arg) {
process(arg); // ALWAYS calls lvalue version β arg is named, so it's an lvalue!
}
wrapper(42); // 42 is rvalue, but process gets an lvalue
The Solution β Universal References + std::forward:
template<typename T>
void wrapper(T&& arg) { // && with template = UNIVERSAL REFERENCE
process(std::forward<T>(arg)); // forwards as-is
}
std::string s = "hello";
wrapper(s); // T = string&, forwards as lvalue
wrapper(std::string("hi")); // T = string, forwards as rvalue
Reference Collapsing Rules:
T& & β T& (lvalue ref to lvalue ref β lvalue ref)
T& && β T& (rvalue ref to lvalue ref β lvalue ref)
T&& & β T& (lvalue ref to rvalue ref β lvalue ref)
T&& && β T&& (rvalue ref to rvalue ref β rvalue ref)
π Why This Logic:
std::forwardis a conditional cast. If the original argument was an lvalue, it forwards as lvalue. If rvalue, it forwards as rvalue. This preserves the callerβs intent perfectly.
7.2 Factory Pattern & emplace_back
Factory Pattern with Perfect Forwarding:
template<typename T, typename... Args>
std::unique_ptr<T> make_unique_custom(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
auto p = make_unique_custom<Widget>(42, "hello", 3.14);
emplace_back Uses Perfect Forwarding:
std::vector<std::pair<std::string, int>> v;
// push_back: construct pair, then copy/move into vector
v.push_back(std::make_pair("alice", 90));
// emplace_back: construct pair IN PLACE inside vector
v.emplace_back("alice", 90); // forwards args to pair's constructor β no copies!
8 β Copy/Move Elision (RVO/NRVO)
8.1 Return Value Optimization
Definition: The compiler can eliminate copy/move entirely β constructing the object directly in the callerβs stack frame.
std::string createString() {
return std::string("hello"); // RVO: Return Value Optimization
// Object constructed directly in caller's stack frame
}
std::string createNamed() {
std::string s = "hello";
return s; // NRVO: Named Return Value Optimization
}
std::string result = createString(); // NO copy, NO move β zero overhead
C++17 guarantees RVO (mandatory copy elision) for returning prvalues. NRVO is optional but nearly all compilers implement it. Never return std::move(x) β it prevents these optimizations!
9 β Summary Table
9.1 When Things Are Copied vs Moved
| Situation | What Happens |
|---|---|
| Return local by value | RVO (elided) |
Return with std::move |
Move (worse!) |
Pass lvalue to T param |
Copy |
Pass std::move(lv) to T |
Move |
Pass temporary to T param |
Move |
push_back(lvalue) |
Copy |
push_back(std::move(lv)) |
Move |
emplace_back(args...) |
Construct in-place |
Practice Questions
Q1. Explain the difference between an lvalue and an rvalue. Give 3 examples of each.
Q2. Write a complete class DynamicArray with all Rule of Five members (destructor, copy/move constructor, copy/move assignment). Test that moves are O(1).
Q3. What does std::move actually do? Why doesnβt it βmoveβ anything?
Q4. Why should you NEVER write return std::move(x) from a function? What optimization does it prevent?
Q5. Explain noexcept for move operations. What happens to std::vector::push_back if the move constructor is not noexcept?
Q6. What is the Rule of Zero? Rewrite a class that currently uses Rule of Five to follow Rule of Zero instead.
Q7. Explain universal references (T&& in template context). How do they differ from rvalue references?
Q8. What does std::forward do? Write a function that demonstrates perfect forwarding.
Q9. Explain reference collapsing rules. What does T& && collapse to? Why?
Q10. Compare push_back(std::move(x)) vs emplace_back(args...). When is each appropriate?
Key Takeaways
- Move semantics transfer resources instead of copying β O(1) instead of O(n)
std::moveis just a cast β it doesnβt move anything itself- Donβt move return values β let RVO/NRVO do its job
- Always mark moves
noexceptβ STL containers require it for optimization - Use
std::forwardin templates for perfect forwarding emplace_back>push_backβ constructs in place- Rule of Zero β prefer it over Rule of Five
- Donβt use moved-from objects except to assign or destroy