Move Semantics — Rvalue References, Move Constructors, Move Assignment, std::move
In this tutorial, you will learn about Move Semantics. We cover key concepts, practical examples, and best practices to help you master this topic.
C++ move semantics enable transferring ownership of resources (heap memory, file handles, sockets) from temporary objects to permanent ones, replacing expensive deep copies with cheap pointer swaps.
What You'll Learn
You will understand rvalue references and value categories (lvalue, prvalue, xvalue), write move constructors and move assignment operators, use std::move to cast to rvalue references, implement the rule of five for resource-managing classes, and apply move semantics in STL containers, std::unique_ptr, and performance-critical code.
Why It Matters
Move semantics eliminate the biggest performance problem in C++98: unnecessary copies. Returning a large std::vector from a function used to copy all elements; now it moves them in O(1). Every modern C++ library — including the STL — relies on move semantics for performance. Understanding moves is essential for writing efficient, modern C++.
Learning Path
graph LR
A["51: auto & decltype"] --> B["52: Move Semantics"]
B --> C["53: Perfect Forwarding"]
C --> D["54: Structured Bindings"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
Value Categories
Every expression in C++ is one of three value categories:
- lvalue: has an identity, address can be taken (variables, references)
- prvalue (pure rvalue): temporary, no identity (literals, function returns)
- xvalue (eXpiring value): about to expire, resources can be stolen (result of
std::move)
#include <iostream>
#include <string>
#include <utility>
void category(const std::string&) {
std::cout << "lvalue reference\n";
}
void category(std::string&&) {
std::cout << "rvalue reference\n";
}
int main() {
std::string s = "hello";
category(s); // lvalue reference (s is an lvalue)
category("world"); // rvalue reference (string literal → temporary)
category(std::move(s)); // rvalue reference (cast to xvalue)
// After move: s is valid but unspecified (typically empty)
std::cout << "s after move: '" << s << "'\n";
}
Rvalue References (T&&)
Rvalue references bind only to rvalues — temporaries and expiring values.
#include <iostream>
#include <string>
void process(int& x) {
std::cout << "Lvalue: " << x << "\n";
}
void process(int&& x) {
std::cout << "Rvalue: " << x << "\n";
}
int main() {
int a = 10;
process(a); // Lvalue
process(20); // Rvalue
process(std::move(a)); // Rvalue (cast to xvalue)
process(a + 5); // Rvalue (temporary result)
}
Move Constructor and Move Assignment
The move constructor "steals" resources from a temporary object, leaving it in a valid-but-empty state.
#include <iostream>
#include <cstring>
#include <algorithm>
class Buffer {
char* data_;
size_t size_;
public:
// Constructor
Buffer(size_t size) : data_(new char[size]), size_(size) {
std::fill(data_, data_ + size_, 0);
std::cout << "Constructed, size=" << size_ << "\n";
}
// Destructor
~Buffer() {
delete[] data_;
std::cout << "Destroyed, size=" << size_ << "\n";
}
// Copy constructor (deep copy)
Buffer(const Buffer& other) : data_(new char[other.size_]), size_(other.size_) {
std::copy(other.data_, other.data_ + size_, data_);
std::cout << "Copy constructed, size=" << size_ << "\n";
}
// Move constructor (steal resources)
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
std::cout << "Move constructed, size=" << size_ << "\n";
}
// Move assignment
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
std::cout << "Move assigned, size=" << size_ << "\n";
}
return *this;
}
size_t size() const { return size_; }
};
int main() {
Buffer b1(100); // Constructed
Buffer b2 = b1; // Copy constructed
std::cout << "b2 size: " << b2.size() << "\n"; // 100
Buffer b3 = std::move(b1); // Move constructed
std::cout << "b1 size: " << b1.size() << "\n"; // 0 (moved-from)
std::cout << "b3 size: " << b3.size() << "\n"; // 100
b2 = std::move(b3); // Move assigned
std::cout << "b3 size: " << b3.size() << "\n"; // 0 (moved-from)
}
The Rule of Five
If a class manages a resource (raw pointer, handle, etc.), you should implement all five special member functions:
class Resource {
public:
// 1. Destructor
~Resource();
// 2. Copy constructor
Resource(const Resource&);
// 3. Copy assignment
Resource& operator=(const Resource&);
// 4. Move constructor
Resource(Resource&&) noexcept;
// 5. Move assignment
Resource& operator=(Resource&&) noexcept;
};
If you implement any of these, you likely need all five. For classes that don't manage resources directly (members handle their own cleanup), follow the Rule of Zero: define none of them.
std::move — What It Actually Does
std::move(x) does NOT move anything. It just casts x to an rvalue reference (xvalue) so that move operations can be selected.
#include <iostream>
#include <string>
#include <utility>
int main() {
std::string a = "Hello, this is a long string";
std::string b;
// std::move just casts to T&& — no data movement here
std::cout << "Before move: a = '" << a << "'\n";
// The actual movement happens in the move assignment operator
b = std::move(a);
std::cout << "After move: a = '" << a << "'\n"; // empty or unspecified
std::cout << "After move: b = '" << b << "'\n"; // "Hello, this is a long string"
}
After std::move, the source object is in a valid but unspecified state. It must be destructible and assignable, but nothing else is guaranteed.
Move Semantics with STL
STL containers are move-aware, making operations like returning large containers efficient.
#include <iostream>
#include <vector>
#include <string>
// Building a large vector
std::vector<int> buildLargeVector(size_t n) {
std::vector<int> v;
v.reserve(n);
for (size_t i = 0; i < n; ++i) {
v.push_back(static_cast<int>(i));
}
// In C++11: this return uses move semantics (no copy!)
return v;
}
int main() {
// Construct in place (no copy)
std::vector<std::string> strings;
strings.emplace_back("Constructed in place, no temporary");
// Inserting with std::move
std::string temp = "Moving this into the vector";
strings.push_back(std::move(temp));
// temp is now empty
// Returning large objects from functions
auto big = buildLargeVector(1000000); // O(1) move, not O(n) copy
std::cout << "Size: " << big.size() << "\n"; // 1000000
// Swapping is efficient (moves, not copies)
std::vector<int> v1(100), v2(200);
v1.swap(v2); // swaps internal pointers, O(1)
std::cout << "v1 size: " << v1.size() << "\n"; // 200
}
Move-Only Types
Some types cannot be copied but can be moved.
#include <iostream>
#include <memory>
#include <vector>
int main() {
// std::unique_ptr is move-only
auto ptr = std::make_unique<int>(42);
// auto copy = ptr; // Error: unique_ptr is not copyable
auto moved = std::move(ptr); // OK: transfers ownership
// ptr is now null
std::cout << "moved: " << *moved << "\n"; // 42
std::cout << "ptr is null: " << (ptr == nullptr) << "\n"; // 1
// Store move-only types in containers
std::vector<std::unique_ptr<int>> vec;
vec.push_back(std::make_unique<int>(1));
vec.push_back(std::make_unique<int>(2));
auto extracted = std::move(vec[0]); // Move unique_ptr out of vector
std::cout << "extracted: " << *extracted << "\n"; // 1
}
When Moves Happen Automatically
The compiler generates move operations automatically in many cases.
#include <iostream>
#include <string>
#include <vector>
struct Person {
std::string name;
int age;
// Compiler-generated move constructor/assignment since all members are movable
};
int main() {
Person p1{"Alice", 30};
Person p2 = std::move(p1); // Uses implicitly-declared move constructor
// Automatic move in return
auto makePerson = []() -> Person {
Person p{"Bob", 25};
return p; // Move or NRVO (named return value optimization)
};
Person p3 = makePerson();
// Automatic move when passing temporary
std::vector<std::string> v;
v.push_back("temporary"); // Move (temporary is rvalue)
// v.push_back(s); // Copy (s is lvalue)
// v.push_back(std::move(s)); // Move (cast to rvalue)
}
Common Mistakes
Mistake 1: Using moved-from objects
std::string s = "hello";
std::vector<std::string> v;
v.push_back(std::move(s));
std::cout << s.size(); // Probably 0, but unspecified. Don't do this!
Only destroy or reassign moved-from objects. Do not assume their state.
Mistake 2: Forgetting noexcept on move operations
class Bad {
public:
Bad(Bad&& other) /* missing noexcept */ {
// STL containers prefer copy over throwing move for strong exception guarantee
}
};
Always mark move constructors and move assignment noexcept so STL containers use them.
Mistake 3: Move instead of forward
template <typename T>
void wrapper(T&& arg) {
// Wrong: arg is always an lvalue (has a name)
target(std::move(arg)); // Forces move even for lvalue inputs
// Correct: preserves value category
target(std::forward<T>(arg));
}
Use std::forward for forwarding references, not std::move.
Mistake 4: Self-move assignment not checked
Buffer& operator=(Buffer&& other) noexcept {
// Without self-check, we'd delete our own data then steal it
if (this != &other) { ... }
return *this;
}
Mistake 5: Returning std::move from a function
std::vector<int> create() {
std::vector<int> v = {1, 2, 3};
return std::move(v); // Anti-pattern! Prevents NRVO
// Just: return v; // Compiler will use move or NRVO
}
return v; already uses move semantics. Adding std::move inhibits copy elision.
Practice Questions
- What is the output?
std::vector<int> v(100);
auto v2 = std::move(v);
std::cout << v.size();
Answer: 0 — v is moved-from, its size is now 0 (or unspecified, but typically 0).
What does
std::moveactually do? Answer: It casts its argument to an rvalue reference (xvalue). No data movement happens until a move constructor/assignment is called.When should you mark move operations
noexcept? Answer: Always. STL containers usestd::move_if_noexceptand prefer copy over a potentially throwing move.What is the Rule of Five? Answer: If you implement any of destructor, copy constructor, copy assignment, move constructor, or move assignment, you typically need all five.
Why is
return v;better thanreturn std::move(v);? Answer:return v;allows copy elision (NRVO).return std::move(v);forces move and prevents elision.
FAQ
Mini Project
Implement a move-only FileDescriptor RAII wrapper that manages a POSIX file descriptor:
#include <iostream>
#include <utility>
#include <unistd.h>
#include <fcntl.h>
// Your FileDescriptor class with move semantics
int main() {
// Open a file
FileDescriptor fd1(open("test.txt", O_CREAT | O_WRONLY, 0644));
if (fd1) {
write(fd1.get(), "Hello\n", 6);
}
// Transfer ownership
FileDescriptor fd2 = std::move(fd1);
// fd1 is now closed (moved-from)
// fd1 is no longer valid
// write(fd1.get(), "data", 4); // Would fail: fd1 is closed
// fd2 automatically closes on destruction
}
This project mirrors how C++ standard library handles resource ownership — std::unique_ptr, std::ifstream, and many other RAII types use move semantics. Compare with Java where all objects are reference types and moves are unnecessary.
What's Next
You now understand move semantics — the foundation of efficient C++ resource management. Next, you will learn perfect forwarding, which preserves value categories through template functions using std::forward.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro