Move Semantics & std::move
Understand the problem move semantics solves, how std::move is a cast (not a move), implement move constructors and move assignment, and learn why noexcept is critical for container performance.
The Problem Move Solves
Before C++11, returning a large object from a function required copying it. Copying a std::vector with 1 million elements means allocating new memory, copying every element — O(n) work that is completely wasted if the source is about to be destroyed.
Move semantics solve this: instead of copying, we steal the source's internal resources (its pointer, size, capacity) and leave the source in a valid but empty state. Moving a vector is O(1) — just swap three pointers/integers. The source is not destroyed; it becomes an empty vector.
The key insight: if an object is a temporary or has been marked as "I don't need this anymore" (via std::move), it is safe to steal its resources.
std::move Is a Cast, Not a Move
std::move(x) does not move anything. It is an unconditional cast to an rvalue reference (static_cast). It tells the compiler: "I am done with this object — you may treat it as a temporary."
The actual move happens when a move constructor or move assignment operator is called with the resulting rvalue reference. If the type has no move operations (e.g., const objects), std::move silently falls back to a copy.
This is a common source of bugs: const std::string s = "hello"; auto s2 = std::move(s); copies s because std::move(s) produces const std::string&&, which binds to the copy constructor's const std::string& parameter.
Implementing Move Constructor & Move Assignment
A move constructor steals resources from the source. A move assignment operator releases its own resources, then steals from the source. Both must leave the source in a valid, destructible state.
#include <iostream>
#include <cstring>
#include <utility>
#include <algorithm>
class Buffer {
char* data_;
std::size_t size_;
public:
// Constructor
explicit Buffer(std::size_t n) : data_(new char[n]()), size_(n) {
std::cout << "Alloc " << n << " bytes\n";
}
// Destructor
~Buffer() {
delete[] data_;
std::cout << "Free (" << size_ << ")\n";
}
// Copy constructor (expensive — O(n))
Buffer(const Buffer& other) : data_(new char[other.size_]), size_(other.size_) {
std::memcpy(data_, other.data_, size_);
std::cout << "Copy " << size_ << " bytes\n";
}
// Move constructor (cheap — O(1)) — MUST be noexcept
Buffer(Buffer&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr; // leave source in valid state
other.size_ = 0;
std::cout << "Move " << size_ << " bytes\n";
}
// Move assignment — MUST be noexcept
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_; // release own resources
data_ = other.data_; // steal source's resources
size_ = other.size_;
other.data_ = nullptr; // leave source valid
other.size_ = 0;
}
return *this;
}
// Copy assignment (omitted for brevity)
std::size_t size() const { return size_; }
};
int main() {
Buffer a(1'000'000); // Alloc 1000000 bytes
Buffer b = std::move(a); // Move 1000000 bytes — O(1)!
std::cout << "a.size: " << a.size() << '\n'; // 0 (moved-from)
std::cout << "b.size: " << b.size() << '\n'; // 1000000
}std::vector will only move elements during reallocation if the move constructor is noexcept. If the move constructor might throw, vector must copy instead — otherwise, if a move throws halfway through reallocation, the vector would be in an inconsistent state with some elements moved and some not.
This means that omitting noexcept on your move constructor silently degrades vector performance from O(1) per element to O(n). This is one of the most common performance bugs in C++.
```cpp
// BAD — vector will COPY during reallocation
Widget(Widget&& other) { ... }
// GOOD — vector will MOVE during reallocation
Widget(Widget&& other) noexcept { ... }
```
Always use static_assert(std::is_nothrow_move_constructible_v to verify.
The Moved-From State
After a move, the source object is in a valid but unspecified state. The standard guarantees:
- The object can be destroyed safely.
- The object can be assigned to (giving it a new value).
- For standard library types, all operations without preconditions work (e.g., size(), empty(), clear()).
The standard does NOT guarantee the moved-from object is empty. In practice, std::string and std::vector are typically empty after a move, but this is an implementation detail.
Production rule: after moving from an object, do not read its value — only destroy it or assign a new value.
When the Compiler Auto-Generates Moves
The compiler generates move operations only if the class has no user-declared copy operations, no user-declared move operations, and no user-declared destructor (Rule of Five/Zero). If you declare ANY of these, you should declare ALL of them.
#include <string>
#include <vector>
#include <iostream>
// GOOD: Rule of Zero — compiler generates everything
struct Employee {
std::string name;
std::vector<std::string> skills;
int id;
// No user-declared special members — compiler generates all 5
};
// BAD: Rule of Five violation — destructor suppresses moves
struct Resource {
int* data;
Resource() : data(new int(42)) {}
~Resource() { delete data; } // move operations NOT generated!
// This class will COPY where it could move
// Fix: declare all five special members
};
int main() {
Employee e1{"Alice", {"C++", "Rust"}, 1};
Employee e2 = std::move(e1); // compiler-generated move
std::cout << e2.name << '\n'; // "Alice"
static_assert(std::is_nothrow_move_constructible_v<Employee>,
"Employee should be nothrow-movable");
// static_assert(std::is_nothrow_move_constructible_v<Resource>,
// "FAILS — Resource has no move constructor");
}1. Follow the Rule of Zero — let the compiler generate special members by using RAII types (string, vector, unique_ptr) as members.
2. Always mark move operations noexcept — otherwise std::vector won't use them.
3. std::move on a const object silently copies — watch for this in code reviews.
4. Don't move from objects you still need — use std::move only at the last use.
5. Don't return std::move(local) — this prevents copy elision (NRVO). Just return the local by name.
6. Verify with static_assert(is_nothrow_move_constructible_v.
- Moving steals resources (O(1)) instead of copying them (O(n))
std::moveis just a cast to T&& — the actual move happens in the move constructor/assignment- Move constructors and move assignment must be
noexceptfor vector to use them - Moved-from objects are in a valid but unspecified state — only destroy or reassign
- The compiler auto-generates moves only if no copy/move/destructor is user-declared (Rule of Zero)
Quiz — Test Your Knowledge
(20 XP)1. What does `std::move(x)` actually do?
2. Why must move constructors be marked `noexcept`?
3. What happens with `const std::string s = "hi"; auto s2 = std::move(s);`?