Move Semantics Deep Dive
Understanding rvalue references, perfect forwarding, and move operations
The Performance Problem
Before C++11, returning large objects or passing them to functions required expensive copies.
// Pre-C++11: Expensive copy
std::vector<int> create_large_vector() {
std::vector<int> result(1000000);
// fill data...
return result; // Copy! (or rely on RVO)
}
void process(std::vector<int> data); // Takes by value
std::vector<int> v = create_large_vector(); // Copy!
process(v); // Another copy!Value Categories
| Category | Can be moved from? | Example |
|---|---|---|
| lvalue | No (has identity, can be addressed) | int x; x |
| prvalue ("pure" rvalue) | Yes | 42, func() |
| xvalue ("expiring" value) | Yes | std::move(x) |
| rvalue (prvalue + xvalue) | Yes | Temporary or moved values |
Rvalue References
int x = 42;
int& lref = x; // lvalue reference (binds to lvalues only)
int&& rref = 42; // rvalue reference (binds to rvalues only)
int&& rref2 = std::move(x); // xvalue is also an rvalue
// Function overload resolution
void process(int& value); // #1: lvalue reference
void process(int&& value); // #2: rvalue reference
int a = 42;
process(a); // Calls #1
process(42); // Calls #2
process(std::move(a)); // Calls #2Move Semantics in Practice
1. Move Constructor
class Buffer {
size_t size_;
char* data_;
public:
// Copy constructor (expensive)
Buffer(const Buffer& other)
: size_(other.size_), data_(new char[size_]) {
std::copy(other.data_, other.data_ + size_, data_);
}
// Move constructor (cheap!)
Buffer(Buffer&& other) noexcept
: size_(other.size_), data_(other.data_) {
other.size_ = 0; // Leave source in valid state
other.data_ = nullptr;
}
~Buffer() { delete[] data_; }
};2. Using std::move
Buffer create_buffer(); // Returns by value
Buffer a = create_buffer(); // Move (or elision)
Buffer b = a; // Copy! (a is an lvalue)
Buffer c = std::move(a); // Move! (cast to rvalue)
// After move, 'a' is in a valid but unspecified state
// Don't use 'a' except to assign to or destroyPerfect Forwarding
Preserving value category when passing arguments through wrapper functions.
// Problem: Without forwarding, we lose value category
template<typename T>
void wrapper_bad(T arg) { // Always copies!
process(arg);
}
// Solution: Universal references + std::forward
template<typename T>
void wrapper_good(T&& arg) { // Universal reference
process(std::forward<T>(arg)); // Preserves lvalue/rvalue
}
// Usage
int x = 42;
wrapper_good(x); // T is int&, forwards as lvalue
wrapper_good(42); // T is int, forwards as rvalue
wrapper_good(std::move(x)); // T is int, forwards as rvalueUniversal vs Rvalue References
| Context | T&& means... | Example |
|---|---|---|
| Template parameter | Universal reference | template<typename T> void f(T&&) |
| Concrete type | Rvalue reference | void f(std::string&&) |
| Auto&& | Universal reference | auto&& x = expr; |
Rule of 0/3/5
- Rule of 0:
If you can define your class without any custom destructors, copy/move operations, prefer using member types that manage their own resources (smart pointers, containers).
- Rule of 3:
If you define a destructor, copy constructor, or copy assignment, you probably need all three.
- Rule of 5:
In C++11+, if you define any of destructor/copy/move operations, consider defining all five, or explicitly defaulting/deleting them.
Common Pitfalls
1. Returning std::move
// BAD: Prevents RVO/NRVO
std::vector<int> create() {
std::vector<int> result;
// ... fill ...
return std::move(result); // Don't do this!
}
// GOOD: Let the compiler optimize
std::vector<int> create() {
std::vector<int> result;
// ... fill ...
return result; // RVO or move - both good
}2. Moving const Objects
const std::vector<int> v = {1, 2, 3};
auto v2 = std::move(v); // Silently copies! (const can't be moved from)
// Always remove const before moving:
auto v2 = std::move(const_cast<std::vector<int>&>(v)); // Dangerous!
// Or better: don't mark it const if you plan to moveBest Practices
- ✓Mark move constructors and move assignment operators noexcept
- ✓Use = default for trivial move operations
- ✓Use = delete to disable move when copy is disabled
- ✓Follow Rule of 0 when possible (use smart pointers/containers)
- ✗Don't use std::move on return values (prevents RVO)
- ✗Don't move from const objects