C++ Move Semantics: Rvalue References, std::move & Perfect Forwarding 2026
Table of Contents
1. The Copy Problem
Before C++11, returning a large object from a function meant copying all its data — even when the original was about to be destroyed:
std::vector<int> createBigVector() {
std::vector<int> v(1'000'000); // 1 million ints
// ... fill v ...
return v; // Pre-C++11: copies 4MB of data!
}
Move semantics solve this by transferring ownership of resources instead of copying them. It’s like giving someone your house keys instead of building them an identical house.
2. Lvalues vs Rvalues
Understanding value categories is essential for move semantics:
int x = 42; // x is an lvalue (has a name, persistent address)
int y = x + 1; // (x + 1) is an rvalue (temporary, no persistent address)
std::string s = "hello";
std::string t = s + " world"; // (s + " world") is an rvalue
// Lvalues: variables, array elements, dereferenced pointers, references
// Rvalues: temporaries, literals, return values from functions
// You can take the address of an lvalue, not an rvalue
int* p = &x; // OK: x is an lvalue
// int* q = &42; // Error: 42 is an rvalue
3. Rvalue References (&&)
C++11 introduced rvalue references (T&&) that can bind to temporaries:
#include <iostream>
#include <string>
void process(const std::string& s) {
std::cout << "lvalue: " << s << "\n";
}
void process(std::string&& s) {
std::cout << "rvalue: " << s << "\n";
// We can safely steal s's resources here
// because the caller doesn't need them anymore
}
int main() {
std::string name = "Alice";
process(name); // calls lvalue overload
process("Bob"); // calls rvalue overload
process(name + " Smith"); // calls rvalue overload
process(std::move(name)); // calls rvalue overload (see section 5)
// name is now in a valid-but-unspecified state
}
4. Move Constructor & Move Assignment
A move constructor steals resources from a dying object instead of copying them:
#include <iostream>
#include <cstring>
#include <utility>
class Buffer {
char* data_;
size_t size_;
public:
// Constructor
explicit Buffer(size_t size)
: data_(new char[size]), size_(size) {
std::cout << "Constructed " << size << " bytes\n";
}
// Copy constructor (expensive)
Buffer(const Buffer& other)
: data_(new char[other.size_]), size_(other.size_) {
std::memcpy(data_, other.data_, size_);
std::cout << "Copied " << size_ << " bytes\n";
}
// Move constructor (cheap — just pointer swap)
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr; // Leave source in valid state
other.size_ = 0;
std::cout << "Moved " << size_ << " bytes\n";
}
// Copy assignment
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data_;
data_ = new char[other.size_];
size_ = other.size_;
std::memcpy(data_, other.data_, size_);
}
return *this;
}
// 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;
}
~Buffer() { delete[] data_; }
size_t size() const { return size_; }
};
int main() {
Buffer b1(1024); // Constructed 1024 bytes
Buffer b2 = b1; // Copied 1024 bytes
Buffer b3 = std::move(b1); // Moved 1024 bytes (fast!)
// b1 is now empty (size 0, null data)
}
noexcept. STL containers like std::vector will only use moves during reallocation if they’re noexcept — otherwise they fall back to copying for exception safety.
5. std::move — Casting to Rvalue
std::move doesn’t actually move anything — it’s just a cast to an rvalue reference, signaling “I’m done with this, you can steal its guts”:
#include <string>
#include <vector>
#include <iostream>
int main() {
std::string source = "Hello, World!";
// std::move casts source to std::string&&
std::string dest = std::move(source);
// source is now empty (moved-from state)
std::cout << "dest: " << dest << "\n"; // "Hello, World!"
std::cout << "source: " << source << "\n"; // "" (empty)
// Moving into containers
std::vector<std::string> words;
std::string word = "efficiency";
words.push_back(word); // copies word
words.push_back(std::move(word)); // moves word (faster)
// word is now empty
// emplace_back constructs in-place (even better)
words.emplace_back("constructed directly");
// Moving from containers
std::string extracted = std::move(words[0]);
// words[0] is now empty but vector still has 3 elements
}
6. Rule of Five Revisited
If you need any of these, you probably need all five (see Rule of 3/5/0):
class Resource {
public:
Resource(); // Constructor
~Resource(); // 1. Destructor
Resource(const Resource&); // 2. Copy constructor
Resource& operator=(const Resource&); // 3. Copy assignment
Resource(Resource&&) noexcept; // 4. Move constructor
Resource& operator=(Resource&&) noexcept; // 5. Move assignment
};
// Rule of Zero: prefer no custom special members
// Use smart pointers and RAII containers
class ModernResource {
std::unique_ptr<Data> data_;
std::vector<int> cache_;
// Compiler generates correct move/copy/destroy automatically!
};
7. Perfect Forwarding
Perfect forwarding preserves the value category (lvalue/rvalue) of arguments through template functions:
#include <utility>
#include <string>
#include <iostream>
#include <memory>
void process(const std::string& s) { std::cout << "lvalue ref\n"; }
void process(std::string&& s) { std::cout << "rvalue ref\n"; }
// Without forwarding: always calls lvalue version
template<typename T>
void wrapperBad(T&& arg) {
process(arg); // arg is named → always an lvalue!
}
// With forwarding: preserves original value category
template<typename T>
void wrapperGood(T&& arg) {
process(std::forward<T>(arg)); // forwards as lvalue or rvalue
}
// 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)...));
}
int main() {
std::string s = "hello";
wrapperBad(s); // lvalue ref ✓
wrapperBad(std::string("temp")); // lvalue ref ✗ (should be rvalue)
wrapperGood(s); // lvalue ref ✓
wrapperGood(std::string("temp")); // rvalue ref ✓
auto p = make_unique_custom<std::string>("forwarded!");
std::cout << *p << std::endl;
}
T&& is only a forwarding (universal) reference when T is deduced. std::string&& is always an rvalue reference, but auto&& and T&& in templates are forwarding references.
8. Real-World Patterns
Sink Parameters
class Logger {
std::vector<std::string> logs_;
public:
// Take by value and move into storage
void addLog(std::string message) {
logs_.push_back(std::move(message));
}
// Called with lvalue: 1 copy + 1 move
// Called with rvalue: 1 move + 1 move
// Called with literal: 1 construction + 1 move
};
Return Value Optimization (RVO)
std::vector<int> generateData() {
std::vector<int> result;
result.reserve(1000);
for (int i = 0; i < 1000; ++i)
result.push_back(i);
return result; // RVO: no copy, no move — constructed in place
// Don't write: return std::move(result); — this PREVENTS RVO!
}
Moving Unique Pointers
#include <memory>
class GameEngine {
std::vector<std::unique_ptr<Entity>> entities_;
public:
void addEntity(std::unique_ptr<Entity> entity) {
entities_.push_back(std::move(entity)); // Must move — can't copy
}
std::unique_ptr<Entity> removeEntity(int index) {
auto entity = std::move(entities_[index]);
entities_.erase(entities_.begin() + index);
return entity; // RVO applies
}
};
9. Practice Exercises
Exercise 1: Move-Aware String
Implement a simple string class with move constructor and move assignment. Count copies vs moves to verify efficiency.
Exercise 2: Object Pool
Create an object pool that uses std::move to return objects and accept them back without copying.
Exercise 3: Perfect-Forwarding Wrapper
Write a timed_call function that measures execution time of any callable with any arguments, using perfect forwarding.
What’s Next?
With move semantics mastered, you’re ready for constexpr & Compile-Time Computing — pushing computation from runtime to compile time for maximum performance.
Return to the C++ Learning Roadmap to continue your journey.