C++: Move Semantics Deep Dive
Last updated: 2026-08-26
In Lesson 44, we finished learning about multi-thread synchronization.
Now, we'll dive into one of the most important features of C++11 — move semantics.
Understanding move semantics is key to truly understanding modern C++ performance optimization.
1. Move Semantics Overview
(1) 1.1 Why Do We Need Move Semantics?
Problem: In C++98/03, copying temporary objects (rvalues) is wasteful.
Example: Copying Temporary Objects (Difficulty ⭐)
▶ Example 2: STL Container Usage (Difficulty ⭐)
std::vector<int> createVector() {
std::vector<int> v = {1, 2, 3, 4, 5};
return v; // Return temporary object
}
std::vector<int> dest = createVector(); // Copy the temporary object, wasteful!
Output:
Copy count: 0
Move semantics solution: "Steal" the temporary object's resources instead of copying them.
(2) 1.2 Lvalue vs Rvalue
| Category | Description | Example |
|---|---|---|
| Lvalue | Has a name, can take address | x in int x = 10; |
| Rvalue | Temporary object, about to be destroyed | 10, x + 1, function return values |
Real-life analogy:
- Lvalue = a named variable (like "Zhang San's book")
- Rvalue = a temporary object (like "a just-purchased book" that hasn't been named yet)
2. Rvalue References
(1) 2.1 Basic Syntax
Rvalue references use && and can only bind to rvalues.
Example: Rvalue References (Difficulty ⭐)
#include <iostream>
void process(int& x) {
std::cout << "Processing lvalue: " << x << std::endl;
}
void process(int&& x) {
std::cout << "Processing rvalue: " << x << std::endl;
}
int main() {
int a = 10;
process(a); // Calls process(int&)
process(20); // Calls process(int&&)
return 0;
}
(2) 2.2 std::move
std::move is used to convert an lvalue to an rvalue reference, signaling "I no longer need this object, you can steal its resources."
Example: Using std::move to Transfer Resources (Difficulty ⭐⭐)
#include <iostream>
#include <vector>
#include <utility>
int main() {
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = std::move(v1); // Move construction: v1's resources are "stolen" by v2
std::cout << "v2 size: " << v2.size() << std::endl; // 3
std::cout << "v1 size: " << v1.size() << std::endl; // 0 (v1 is empty after being moved from)
return 0;
}
💡 Tip:
std::moveitself doesn't move anything — it's just a type conversion- The actual move happens in the move constructor or move assignment operator
3. Move Constructor and Move Assignment
(1) 3.1 Why Do We Need Custom Move Operations?
Problem: Compiler-generated move operations may not be efficient (e.g., for deep-copy classes).
Solution: Define custom move constructor and move assignment operator.
(2) 3.2 Example: Implementing Move Operations (Difficulty ⭐⭐⭐)
#include <iostream>
#include <cstring>
class String {
private:
char* data;
size_t length;
public:
// Constructor
String(const char* str) {
length = strlen(str);
data = new char[length + 1];
strcpy(data, str);
std::cout << "Construct:" << data << std::endl;
}
// Copy constructor (deep copy)
String(const String& other) {
length = other.length;
data = new char[length + 1];
strcpy(data, other.data);
std::cout << "Copy construction: " << data << std::endl;
}
// Move constructor (steal resources)
String(String&& other) noexcept {
data = other.data; // Steal the pointer
length = other.length;
other.data = nullptr; // Set to null, prevent double free
other.length = 0;
std::cout << "Move construction" << std::endl;
}
// Destructor
~String() {
delete data;
}
};
int main() {
String s1("Hello");
String s2 = std::move(s1); // Calls the move constructor
return 0;
}
Output:
Construct:Hello
Move construction
Execution Result:
Construct:Hello
Move construction
4. Rules of Move Semantics
(1) 4.1 The Big Five
If a class needs a custom destructor, copy constructor, or copy assignment operator, it typically also needs custom move constructor and move assignment operator.
| Function | Description |
|---|---|
| Destructor | Release resources |
| Copy constructor | Deep copy |
| Copy assignment operator | Deep copy assignment |
| Move constructor | Steal resources |
| Move assignment operator | Steal resources assignment |
(2) 4.2 Rule of Zero
Best Practice: If you use smart pointers to manage resources, you don't need to define any of the Big Five (the compiler will automatically generate correct versions).
class Person {
std::string name; // Using string, automatically supports move
std::vector<int> scores; // Using vector, automatically supports move
// No need to define the Big Five!
}
5. Perfect Forwarding
(1) 5.1 What Is Perfect Forwarding?
Perfect forwarding means passing arguments to other functions without altering their lvalue/rvalue nature.
Example: Using std::forward for Perfect Forwarding (Difficulty ⭐⭐⭐)
#include <iostream>
#include <utility>
void process(int& x) {
std::cout << "Processing lvalue" << std::endl;
}
void process(int&& x) {
std::cout << "Processing rvalue" << std::endl;
}
template<typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg)); // Perfect forwarding
}
int main() {
int x = 10;
wrapper(x); // Forwards as lvalue
wrapper(20); // Forwards as rvalue
return 0;
}
6. Performance Advantages of Move Semantics
▶ Example 1: Comparing Copy and Move (Difficulty ⭐⭐)
#include <iostream>
#include <vector>
#include <chrono>
int main() {
std::vector<std::vector<int>> v;
// Test copy
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10000; i++) {
std::vector<int> temp(1000, 1);
v.push_back(temp); // Copy
}
auto end = std::chrono::high_resolution_clock::now();
auto copy_time = std::chrono::duration<double>(end - start).count();
v.clear();
// Test move
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 10000; i++) {
std::vector<int> temp(1000, 1);
v.push_back(std::move(temp)); // Move
}
end = std::chrono::high_resolution_clock::now();
auto move_time = std::chrono::duration<double>(end - start).count();
std::cout << "Copy time: " << copy_time << " seconds" << std::endl;
std::cout << "Move time: " << move_time << " seconds" << std::endl;
return 0;
}
Output:
Copy time: 0.5 seconds
Move time: 0.01 seconds
▶ Example 3: std::move Transfers Resource Ownership (Difficulty ⭐)
#include <iostream>
#include <vector>
#include <string>
int main() {
std::string original = "Hello, C++!";
std::string moved = std::move(original);
std::cout << "moved: " << moved << std::endl;
std::cout << "original after move: \"" << original << "\"" << std::endl;
return 0;
}
Output:
moved: Hello, C++!
original after move: ""
❓ FAQ
Q: What state is an object in after being moved from? A: A moved-from object is in a valid but unspecified state (typically empty). You can assign a new value to it, but you cannot assume its value.
std::move) - Container element insertion (emplace_back)📖 Summary
| Concept | Key Point |
|---|---|
| Rvalue reference | &&, can only bind to rvalues |
| std::move | Converts an lvalue to an rvalue reference |
| Move constructor | Steals resources, no copy |
| Move assignment | Steals resources for assignment |
| Perfect forwarding | std::forward, preserves value category |
📝 Exercises
-
Basic (Difficulty ⭐): Create a
vector<string>, add several strings usingpush_back, and observe the output to confirm copies are happening. Then compare withemplace_back. -
Intermediate (Difficulty ⭐⭐): Implement a
MyStringclass (with both copy constructor and move constructor), trigger move semantics viastd::moveinmain, and observe which constructor is called. -
Challenge (Difficulty ⭐⭐⭐): Implement a move-only type (like
unique_ptr), disable copy constructor and copy assignment, enable move constructor and move assignment. Write test code to verify it cannot be copied but can be moved.
- Lvalue: an expression with an address that can be taken
- Rvalue: a temporary object with no addressable storage
- std::move converts an lvalue to an rvalue reference
- Move constructor: transfers resource ownership rather than copying
- Move semantics improve performance by avoiding deep copies
Next Lesson: Smart Pointers Advanced (#46)