ð Lesson 18: Move Semantics and Rvalue References
Optimize your C++ applications by moving resources instead of copying them.
ðŊ Learning Objectives
By the end of this lesson, you will be able to:
- Distinguish between lvalues and rvalues to understand object lifetime and move eligibility.
- Implement move constructors and move assignment operators for efficient resource transfer.
- Use
std::moveandstd::forwardcorrectly to optimize code and enable perfect forwarding. - Apply move semantics to standard containers and custom classes for maximum performance.
Estimated Time: 90â120 minutes
Project: Implement a move-enabled, high-performance buffer class that demonstrates significant performance gains over traditional copy-based implementations.
In This Lesson
The Problem: Expensive Copies
Imagine you're moving houses. The old way (copy semantics) is like buying duplicates of all your furniture for the new house, then throwing away the originals. Move semantics is like taking your actual furniture to the new house - much more efficient!
The Old Way: Copy Everything
class String {
private:
char* data;
size_t length;
public:
// Copy constructor - expensive for large strings!
String(const String& other)
: length(other.length) {
data = new char[length + 1]; // Allocate new memory
std::strcpy(data, other.data); // Copy all characters
}
// Copy assignment - also expensive!
String& operator=(const String& other) {
if (this != &other) {
delete[] data; // Clean up old data
length = other.length;
data = new char[length + 1]; // Allocate new memory
std::strcpy(data, other.data); // Copy all characters
}
return *this;
}
};
Understanding Lvalues and Rvalues
Before diving into move semantics, we need to understand lvalues and rvalues. Think of lvalues as things with addresses (like houses) and rvalues as temporary things (like the pizza delivery guy).
int x = 42; // x is lvalue, 42 is rvalue
int y = x; // y is lvalue, x is lvalue (lvalue in rvalue context)
int& lr = x; // OK: lvalue reference to lvalue
int& lr2 = 42; // ERROR: cannot bind lvalue reference to rvalue
const int& cr = 42; // OK: const lvalue reference can bind to rvalue
int&& rr = 42; // OK: rvalue reference to rvalue
int&& rr2 = x; // ERROR: cannot bind rvalue reference to lvalue
// Function returning by value creates an rvalue
std::string getName() { return "temporary"; }
std::string s = getName(); // getName() returns an rvalue
// But you can't do:
// getName() = "something"; // ERROR: can't assign to rvalue
Rvalue References: The && Syntax
Rvalue references (&&) are like temporary ownership papers. They let you take ownership of resources that are about to be destroyed anyway.
Move Constructor and Move Assignment
Now we can implement efficient move operations that transfer ownership instead of copying data.
class String {
private:
char* data;
size_t length;
public:
// Move constructor - takes ownership of temporary
String(String&& other) noexcept
: data(other.data), length(other.length) {
// "Steal" the resources
other.data = nullptr; // Leave other in valid but empty state
other.length = 0;
}
// Move assignment operator
String& operator=(String&& other) noexcept {
if (this != &other) {
delete[] data; // Clean up current resources
// "Steal" the resources
data = other.data;
length = other.length;
// Leave other in valid but empty state
other.data = nullptr;
other.length = 0;
}
return *this;
}
// Regular constructors and destructor
String(const char* str) {
length = std::strlen(str);
data = new char[length + 1];
std::strcpy(data, str);
}
~String() {
delete[] data;
}
};
The Rule of Three, Five, and Zero
Modern C++ favors the Rule of Zero: let members like std::vector, std::string, and std::unique_ptr own their resources, and your class needs none of the five special members â the compiler-generated ones (including the moves) just work. Reach for the Rule of Five only when your class manages a raw resource directly.
std::move: Casting to Rvalue
std::move doesn't actually move anything! It's just a cast that tells the compiler "treat this lvalue as an rvalue". It's like putting a "free to take" sign on your furniture.
std::vector<int> v1 = {1, 2, 3, 4, 5};
std::vector<int> v2 = v1; // Copy: v1 still has data
std::vector<int> v3 = {1, 2, 3, 4, 5};
std::vector<int> v4 = std::move(v3); // Move: v3 is now empty
// After move, v3 is in valid but unspecified state
// You can still use v3, but don't assume it has any particular value
v3.clear(); // OK
v3.push_back(10); // OK
// Common pattern: moving in function returns
std::vector<int> createVector() {
std::vector<int> local = {1, 2, 3, 4, 5};
return local; // No std::move needed! Compiler does RVO/NRVO
}
// When to use std::move
class Widget {
std::vector<int> data;
public:
void setData(std::vector<int> newData) {
data = std::move(newData); // Move parameter into member
}
};
Perfect Forwarding with Universal References
Universal references (also called forwarding references) can bind to both lvalues and rvalues, preserving their value category. It's like a postal service that handles both regular mail and express packages appropriately.
// Universal reference: T&& where T is deduced
template<typename T>
void process(T&& param) { // Universal reference
// param can be lvalue or rvalue reference
}
// Perfect forwarding: preserve value category
template<typename T>
void wrapper(T&& arg) {
// std::forward preserves whether arg was lvalue or rvalue
actualFunction(std::forward<T>(arg));
}
// Practical example: make_unique before C++14
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
// Usage
auto p1 = make_unique<String>("Hello"); // Forward rvalue
String s = "World";
auto p2 = make_unique<String>(s); // Forward lvalue (copies)
auto p3 = make_unique<String>(std::move(s)); // Forward rvalue (moves)
Move Semantics in Standard Containers
All standard containers support move semantics, making operations like returning containers from functions extremely efficient.
// Efficient container operations
std::vector<std::string> readFile(const std::string& filename) {
std::vector<std::string> lines;
// ... read file into lines ...
return lines; // Move, not copy! (NRVO or move)
}
// Container of containers
std::vector<std::vector<int>> matrix;
std::vector<int> row = {1, 2, 3, 4, 5};
// Old way (copy)
matrix.push_back(row); // row is still valid
// New way (move)
matrix.push_back(std::move(row)); // row is now empty
// Moving elements within containers
std::vector<std::string> v1 = {"apple", "banana", "cherry"};
std::vector<std::string> v2;
// Move all elements
v2 = std::move(v1); // v1 is now empty
// Move individual elements
std::vector<std::unique_ptr<Widget>> widgets;
widgets.push_back(std::make_unique<Widget>()); // Move-only type
// Algorithms with move
std::vector<std::string> source = {"a", "b", "c"};
std::vector<std::string> dest;
std::move(source.begin(), source.end(), std::back_inserter(dest));
Common Patterns and Best Practices
The Copy-and-Swap Idiom
class String {
// ... other members ...
// Unified assignment operator handles both copy and move
String& operator=(String other) { // Pass by value
swap(other);
return *this;
} // other's destructor cleans up our old data
void swap(String& other) noexcept {
std::swap(data, other.data);
std::swap(length, other.length);
}
};
Move-Only Types
class FileHandle {
// A move-aware member (unique_ptr with a custom deleter) owns the FILE*
std::unique_ptr<std::FILE, int(*)(std::FILE*)> file;
public:
explicit FileHandle(const char* path)
: file(std::fopen(path, "r"), &std::fclose) {}
// Delete copy operations
FileHandle(const FileHandle&) = delete;
FileHandle& operator=(const FileHandle&) = delete;
// Defaulted move operations are correct here: moving the unique_ptr member
// transfers ownership and leaves the source holding nullptr
FileHandle(FileHandle&&) noexcept = default;
FileHandle& operator=(FileHandle&&) noexcept = default;
// Now FileHandle can only be moved, not copied â
// perfect for unique resources like file handles
};
When to Use std::move
Practical Examples
ðïļ Exercise: Implement a Move-Enabled Buffer Class
template<typename T>
class Buffer {
private:
T* data;
size_t size;
public:
// Constructor
explicit Buffer(size_t n)
: data(new T[n]), size(n) {}
// Destructor
~Buffer() { delete[] data; }
// TODO: Implement
// 1. Copy constructor
// 2. Copy assignment
// 3. Move constructor
// 4. Move assignment
// Hints:
// - Copy should allocate new memory and copy elements
// - Move should transfer ownership and set source to nullptr
// - Remember to handle self-assignment
// - Mark move operations as noexcept
};
ðïļ Exercise: Perfect Forwarding Factory
// Create a factory function that perfectly forwards arguments
template<typename T, typename... Args>
T* factory(Args&&... args) {
std::cout << "Creating object with "
<< sizeof...(args) << " arguments\n";
// TODO: Use perfect forwarding to construct T
// Hint: Use std::forward<Args>(args)...
}
// Test with:
struct Widget {
Widget(int x) { std::cout << "Widget(int)\n"; }
Widget(const std::string& s) { std::cout << "Widget(string&)\n"; }
Widget(std::string&& s) { std::cout << "Widget(string&&)\n"; }
};
ðïļ Exercise: Move-Only Message Queue
// Implement a thread-safe queue for move-only types
template<typename T>
class MoveOnlyQueue {
private:
std::queue<T> queue;
mutable std::mutex mutex;
public:
// Only allow moving in
void push(T&& value) {
// TODO: Lock mutex and move value into queue
}
// Only allow moving out
std::optional<T> pop() {
// TODO: Lock mutex, check if empty
// If not empty, move front element out
// Return std::nullopt if empty
}
};
Performance Impact
Move semantics can dramatically improve performance, especially for containers and resource-heavy objects.
// Benchmark example
#include <chrono>
#include <vector>
void benchmarkCopyVsMove() {
const size_t SIZE = 1000000;
std::vector<int> source(SIZE, 42);
// Benchmark copy
auto start = std::chrono::high_resolution_clock::now();
std::vector<int> copied = source; // Copy
auto copy_time = std::chrono::high_resolution_clock::now() - start;
// Benchmark move
start = std::chrono::high_resolution_clock::now();
std::vector<int> moved = std::move(source); // Move
auto move_time = std::chrono::high_resolution_clock::now() - start;
std::cout << "Copy time: "
<< std::chrono::duration_cast<std::chrono::microseconds>(copy_time).count()
<< " Ξs\n";
std::cout << "Move time: "
<< std::chrono::duration_cast<std::chrono::microseconds>(move_time).count()
<< " Ξs\n";
// Typical result:
// Copy time: 2000 Ξs
// Move time: 1 Ξs
}
Common Pitfalls and Solutions
Pitfall: Moving from const objects
const std::vector<int> v = {1, 2, 3};
std::vector<int> v2 = std::move(v); // Doesn't move! Copies instead!
// std::move on const object returns const T&&, which matches copy constructor
Pitfall: Use after move
std::string s = "Hello";
std::string s2 = std::move(s);
std::cout << s; // Undefined behavior? No! s is valid but unspecified (usually empty)
// Standard library types guarantee a valid but unspecified state
Pitfall: Returning std::move
// BAD - prevents RVO
std::vector<int> bad() {
std::vector<int> v = {1, 2, 3};
return std::move(v); // Don't do this!
}
// GOOD - allows RVO/NRVO
std::vector<int> good() {
std::vector<int> v = {1, 2, 3};
return v; // Compiler optimizes
}
ðŊ Quick Quiz
Question 1: Given int x = 42;, which declaration correctly compiles?
Question 2: What does std::move(obj) actually do at runtime?
Question 3: Why is return std::move(localVar); considered bad practice when returning a local by value?
Summary
ð Key Takeaways
- Lvalues have persistent addresses; rvalues are temporaries that are about to be destroyed
- Rvalue references (&&) let you bind to â and take ownership of â temporaries
- Move constructors/assignment transfer ownership of resources instead of copying them, leaving the source in a valid but empty state
- std::move doesn't move anything itself â it's just a cast that makes an lvalue eligible for move overloads
- Universal references (
T&&in a template) plus std::forward enable perfect forwarding of arguments - Never
return std::move(localVar);â it defeats RVO/NRVO and can make your code slower - Move semantics can turn O(n) copies into O(1) pointer swaps for containers and resource-owning classes
ð Additional Resources
- cppreference â std::move
- cppreference â Move constructors
- LearnCpp â Introduction to move semantics
ð What's Next?
Move semantics transformed C++ from a language that copied everything to one that can efficiently transfer resources â like handing over a document instead of photocopying it. Next, in Lesson 19: Multithreading and Concurrency, you'll run code across multiple threads at once, where cheaply moving data between threads becomes essential to writing efficient concurrent code.
ð Move semantics mastered!
You've crossed one of the trickiest thresholds in modern C++ â lvalues, rvalues, and the && syntax trip up even experienced developers. From here on, you'll read and write high-performance C++ with real understanding of what's happening under the hood.