C++ Move Semantics and Perfect Forwarding
Before C++11, passing objects in C++ meant either copying (expensive for large objects) or passing pointers (error-prone lifetime management). Move semantics changed this by allowing resources to be transferred from temporary objects without copying. Combined with perfect forwarding, it enables efficient generic code that preserves value category.
This guide covers rvalue references, move constructors, std::move, perfect forwarding, and practical performance patterns.
Lvalues and Rvalues
Every expression in C++ has a value category. The two fundamental categories are:
- lvalue — An expression that refers to a memory location. It has an address and persists beyond a single expression.
- rvalue — A temporary value that does not persist beyond the expression that creates it.
int x = 42; // x is an lvalue, 42 is an rvalue
int y = x; // y and x are lvalues
int z = x + y; // x + y is an rvalue (temporary)
Rvalue References
An rvalue reference (&&) binds only to rvalues:
void process(int& arg) { // lvalue reference
std::cout << "lvalue\n";
---
void process(int&& arg) { // rvalue reference
std::cout << "rvalue\n";
---
int x = 42;
process(x); // lvalue overload
process(42); // rvalue overload
process(std::move(x)); // rvalue overload (cast)
std::move does not move anything — it casts an lvalue to an rvalue reference, enabling the move overload.
Move Constructors and Move Assignment
The Problem with Copies
class Buffer {
int* data;
size_t size;
public:
// Constructor
explicit Buffer(size_t n) : size(n), data(new int[n]) {}
// Copy constructor — expensive deep copy
Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
std::copy(other.data, other.data + size, data);
}
// Destructor
~Buffer() { delete[] data; }
---;
Buffer create_buffer() {
Buffer b(1000000);
// Fill b...
return b; // Before move semantics: deep copy
---Before move semantics, returning large objects triggered a copy. Compilers could optimize with NRVO (Named Return Value Optimization), but not always.
Move Constructor
class Buffer {
int* data;
size_t size;
public:
explicit Buffer(size_t n) : size(n), data(new int[n]) {}
// Copy constructor
Buffer(const Buffer& other)
: size(other.size), data(new int[other.size]) {
std::copy(other.data, other.data + size, data);
}
// Move constructor — 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; // Free existing resources
data = other.data; // Steal pointer
size = other.size;
other.data = nullptr; // Leave source empty
other.size = 0;
}
return *this;
}
~Buffer() { delete[] data; }
---;The move constructor takes an rvalue reference, copies the pointer, and nullifies the source. No deep copy of a million integers — just two pointer assignments. The noexcept specification is important: standard containers like std::vector use move semantics only if the move operation is noexcept.
The Rule of Five
Any class that manages a resource should define or delete all five special member functions:
class Resource {
public:
~Resource();
Resource(const Resource&); // Copy constructor
Resource& operator=(const Resource&); // Copy assignment
Resource(Resource&&) noexcept; // Move constructor
Resource& operator=(Resource&&) noexcept; // Move assignment
---;If you define a custom destructor, copy constructor, or copy assignment, you likely need all five. Or use the rule of zero: let RAII wrapper classes handle resources.
When Move Is Used
Standard Library Containers
std::vector<std::string> v;
std::string s = "hello world this is a long string";
v.push_back(s); // Copy — s remains valid
v.push_back(std::move(s)); // Move — s is now empty
// s is in a valid but unspecified state
std::vector reallocates when it grows. With move semantics, elements are moved instead of copied during reallocation:
std::vector<BigObject> v;
v.reserve(2);
v.emplace_back(/* ... */);
v.emplace_back(/* ... */);
v.emplace_back(/* ... */); // Triggers reallocation — moves elements
Returning from Functions
// RVO (Return Value Optimization) or move
std::vector<int> create_large_vector() {
std::vector<int> result(1000000);
// Fill result...
return result; // No copy — RVO or implicit move
---
auto v = create_large_vector(); // Efficient
The compiler first attempts RVO (eliding the copy/move entirely). If RVO does not apply, it falls back to an implicit move.
std::swap with Move
template <typename T>
void my_swap(T& a, T& b) {
T temp = std::move(a); // Move a into temp
a = std::move(b); // Move b into a
b = std::move(temp); // Move temp into b
---For types like std::string or std::vector, swap becomes O(1) — just pointer swaps.
Perfect Forwarding
Perfect forwarding preserves the value category (lvalue/rvalue) of arguments passed through a function:
template <typename T>
void wrapper(T&& arg) {
// Forward with perfect forwarding
target_function(std::forward<T>(arg));
---Why Forwarding Matters
void process(int& x) { std::cout << "lvalue\n"; }
void process(int&& x) { std::cout << "rvalue\n"; }
template <typename T>
void bad_forward(T&& arg) {
process(arg); // Always calls lvalue overload — arg is named
---
template <typename T>
void good_forward(T&& arg) {
process(std::forward<T>(arg)); // Preserves value category
---
int x = 42;
good_forward(x); // lvalue
good_forward(42); // rvalue
good_forward(std::move(x)); // rvalue
Universal References (Forwarding References)
T&& in a deduced context is a forwarding reference, not an rvalue reference:
template <typename T>
void f(T&&); // Forwarding reference
auto&& x = expr; // Forwarding reference (decltype(auto) style)
template <typename T>
void g(std::vector<T>&&); // Rvalue reference — NOT forwarding
The distinction: T&& in a deduced template parameter is a forwarding reference. T&& in a non-deduced context (like std::vector<T>&&) is an rvalue reference.
std::forward Implementation
template <typename T>
T&& forward(std::remove_reference_t<T>& arg) noexcept {
return static_cast<T&&>(arg);
---When T is int&, T&& collapses to int& (lvalue). When T is int, T&& is int&& (rvalue). Reference collapsing rules make this work:
| T | T&& | Result | |
Rvalue References in Depth
The rvalue reference (&&) is the language feature that enables move semantics. It binds exclusively to rvalues — temporary objects or objects explicitly cast with std::move. This allows the compiler to distinguish between “I want a copy” (lvalue) and “I want to steal resources” (rvalue):
void process(std::vector<int> &&data) {
// data is an rvalue reference — caller doesn't need it anymore
// We can steal its internal buffer
---
std::vector<int> v(1000000);
process(std::move(v)); // v's buffer is moved, not copied
// v is now empty but valid
After a move, the source object is left in a valid but unspecified state. It can be destroyed or assigned to, but its contents should not be assumed.
Move Constructors and Move Assignment
A move constructor transfers resources from a source object to a new object without copying:
class Buffer {
char *data;
size_t size;
public:
// Move constructor
Buffer(Buffer &&other) noexcept
: data(std::exchange(other.data, nullptr))
, size(std::exchange(other.size, 0)) {}
// Move assignment
Buffer &operator=(Buffer &&other) noexcept {
if (this != &other) {
delete[] data; // Release current resources
data = std::exchange(other.data, nullptr); // Steal
size = std::exchange(other.size, 0);
}
return *this;
}
---;The noexcept specifier is critical — std::vector will only use the move constructor when reallocating if it is noexcept. Otherwise, it falls back to copying to maintain the strong exception guarantee.
When Move Falls Back to Copy
Not all types benefit from move semantics. Fundamental types (int, double, pointers) and types without dynamically allocated resources are copied even when passed through std::move. The move is no more efficient than a copy for these types. Move semantics provide the greatest benefit for types that manage heap memory, file handles, or other dynamically acquired resources.
Perfect Forwarding
Perfect forwarding preserves the value category of arguments through template functions:
template<typename T, typename... Args>
std::unique_ptr<T> make_wrapper(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
---std::forward conditionally casts to an rvalue reference — if the original argument was an rvalue, the forwarded argument is also an rvalue. This enables factory functions and wrapper templates to preserve move semantics through multiple layers of function calls.
FAQ
What is the difference between malloc and new in C++? malloc allocates raw memory without calling constructors; new allocates memory and calls the constructor. In C++, prefer new for objects. free vs delete follows the same pattern — delete calls the destructor.
How do I prevent memory leaks in C/C++? Use RAII (Resource Acquisition Is Initialization) in C++ — smart pointers like std::unique_ptr and std::shared_ptr automatically free memory. In C, always pair every malloc with a free and use tools like Valgrind or AddressSanitizer to detect leaks.
What is undefined behavior in C/C++? Undefined behavior occurs when code performs operations that the language standard does not define — dereferencing a null pointer, buffer overflow, signed integer overflow. The compiler may generate any code, including unexpected results or crashes.
Should I learn C or C++ first? Learn C first if you want to understand low-level memory and system programming. Learn C++ first if you want object-oriented features and the STL. Both are valuable; C++ builds on C concepts.
What is the difference between a header file and a source file? Header files (.h) declare interfaces — function prototypes, class definitions, macros. Source files (.c or .cpp) implement the declarations. Headers are #included; source files are compiled separately and linked.
—|—–|——–|
| int& | int& && | int& |
| int&& | int&& && | int&& |
| int | int&& | int&& |
Reference Collapsing Rules
using Lref = int&;
using Rref = int&&;
int x = 42;
Lref& r1 = x; // int& — lvalue ref to lvalue ref
Lref&& r2 = x; // int& — lvalue ref to rvalue ref
Rref& r3 = x; // int& — rvalue ref to lvalue ref
Rref&& r4 = 42; // int&& — rvalue ref to rvalue ref
The rule: & always wins. An && followed by & collapses to &. Only && + && = &&.
Practical Patterns
Emplace Back
std::vector<std::pair<std::string, int>> v;
// Constructs pair<string, int>, then copies/moves into vector
v.push_back(std::pair<std::string, int>("hello", 42));
// Constructs pair directly in place — no temporary
v.emplace_back("hello", 42);emplace_back forwards its arguments to the element’s constructor, avoiding the temporary object entirely.
Move-Only Types
std::unique_ptr<int> ptr = std::make_unique<int>(42);
std::vector<std::unique_ptr<int>> v;
// v.push_back(ptr); // Error: unique_ptr is not copyable
v.push_back(std::move(ptr)); // OK: transfer ownership
// ptr is now nullptr
Move-only types must be moved, not copied. This enforces unique ownership.
Factory Functions
template <typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(
new T(std::forward<Args>(args)...)
);
---Perfect forwarding preserves argument value categories through variadic templates.
Performance Considerations
When Move Is Not Faster
- Small types —
int,double,char— copying is as fast as moving - Trivially copyable types —
std::array<int, 3>— memcpy equals move - SSO strings — Short string optimization means small strings (< 15 chars) are copied even with
std::move
Move Semantics with Compiler Optimizations
// RVO (guaranteed in C++17)
auto obj = createObject(); // No copy, no move — constructed in place
// NRVO (optional)
Buffer b = createBuffer(); // May elide copy/move entirely
C++17 guarantees copy elision for prvalues. NRVO is still optional but widely implemented.
Best Practices
- Mark move operations
noexcept— Containers likestd::vectorrequire noexcept moves for optimal reallocation behavior - Leave moved-from objects in a valid state — Typically empty or default-constructed. Document the post-condition.
- Use
std::moveon expensive-to-copy types — Strings, vectors, and other heap-allocated types benefit. Do not usestd::moveon small types. - Do not use
std::moveonconstobjects —const T&&will bind to the copy constructor, not the move constructor. - Prefer
std::forwardfor templates — Usestd::movefor concrete types,std::forwardfor template parameters.
Summary
Move semantics eliminate expensive deep copies for temporary objects. Rvalue references distinguish between modifiable lvalues and temporaries. Move constructors transfer resources by pointer swap instead of deep copy. Perfect forwarding with std::forward preserves value categories through generic code. The result is C++ code that is both safer and faster — no raw pointer passing, no unnecessary copies, and compile-time guarantees about resource ownership.
See also: C vs C++: Key Differences and When to Use Which.
See also: C++ File I/O: Reading and Writing Files.