move in C++ refers to move semantics and the std::move
utility that let you transfer resources from one object to another instead of copying, enabling big performance wins for large or non-copyable objects. std::move
doesn’t move by itself—it casts to an rvalue so a move constructor or move assignment can run.
This beginner-friendly guide explains move in C++ with clear code, outputs, pitfalls, best practices, and “Try it yourself” challenges after every section so you can master modern, efficient C++.
🔹 What is std::move and move semantics?
Move semantics let an object transfer ownership of its resources (like heap memory, file handles) to another object instead of duplicating them. std::move(expr)
is a cast to an rvalue reference, telling the compiler “it’s safe to steal from this object now.” The actual move happens only if a move constructor or move assignment operator is available.
#include <utility> // std::move
#include <string>
#include <iostream>
using namespace std;
int main() {
string a = "Hello, world!";
string b = std::move(a); // moves contents of 'a' into 'b'
cout << "b=" << b << "\n";
cout << "a.size()=" << a.size() << "\n"; // 'a' is valid but unspecified
}
Output
b=Hello, world!
a.size()=0
Moved-from objects remain valid but in an unspecified state (often empty). Only use them for destruction, reassignment, or operations documented to work on empty states.
Try it yourself
- Create a
std::vector<int>
with 1e6 elements, thenauto v2 = std::move(v1)
. Print sizes before/after to see how fast moving is. - Call
std::move
on aconst std::string
and see why it still copies (moving from const usually falls back to copy).
🔹 Implementing move: move constructor and move assignment
To support move in C++, define a move constructor and move assignment that “steal” the resource and null the source. Mark them noexcept
when possible so containers prefer moves during reallocation.
#include <algorithm>
#include <iostream>
using namespace std;
class Buffer {
size_t n{0};
int* p{nullptr};
public:
Buffer() = default;
explicit Buffer(size_t n_) : n(n_), p(new int[n_]{}) {}
~Buffer() { delete[] p; }
// Copy semantics
Buffer(const Buffer& other) : n(other.n), p(new int[n]) {
std::copy(other.p, other.p + n, p);
cout << "copy ctor\n";
}
Buffer& operator=(const Buffer& other) {
if (this != &other) {
int* np = new int[other.n];
std::copy(other.p, other.p + other.n, np);
delete[] p;
p = np;
n = other.n;
cout << "copy assign\n";
}
return *this;
}
// Move semantics
Buffer(Buffer&& other) noexcept : n(other.n), p(other.p) {
other.n = 0; other.p = nullptr;
cout << "move ctor\n";
}
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] p;
n = other.n;
p = other.p;
other.n = 0; other.p = nullptr;
cout << "move assign\n";
}
return *this;
}
size_t size() const { return n; }
};
int main() {
Buffer a(1000000);
Buffer b = std::move(a); // move ctor
Buffer c;
c = std::move(b); // move assign
cout << "a.size=" << a.size()
<< ", b.size=" << b.size()
<< ", c.size=" << c.size() << "\n";
}
Output
move ctor
move assign
a.size=0, b.size=0, c.size=1000000
Notice how moves avoid expensive allocations/copies. Containers can leverage this to reallocate efficiently when your move operations are noexcept
.
Try it yourself
- Remove
noexcept
and push manyBuffer
objects intostd::vector
. Inspect whether reallocation chooses copy over move in your toolchain. - Add a throwing move (for learning) and watch how containers behave; then restore
noexcept
.
🔹 move in C++ with move-only types (unique_ptr)
Some types are intentionally non-copyable but movable, like std::unique_ptr
. You cannot copy them, but you can transfer ownership with std::move
.
#include <memory>
#include <iostream>
using namespace std;
int main() {
auto up = make_unique<int>(42);
// auto up2 = up; // ERROR: unique_ptr is non-copyable
auto up2 = std::move(up); // OK: up becomes empty
cout << boolalpha << (up == nullptr) << " " << *up2 << "\n";
}
Output
true 42
Moving unique resources is often the primary reason to use move in C++. It makes ownership transfer explicit and safe.
Try it yourself
- Store
std::unique_ptr<int>
in astd::vector
. Useemplace_back(std::make_unique<int>(…))
and confirm it compiles and transfers ownership. - Write a function that returns
std::unique_ptr<T>
; move its return value into a caller-owned variable.
🔹 Using std::move correctly in containers (push_back vs emplace_back)
When you push objects into containers, you can avoid copies by moving. emplace_back
constructs in place, often better than constructing then moving.
#include <vector>
#include <string>
#include <iostream>
using namespace std;
int main() {
vector<string> v;
string s = "abcdefghijklmnopqrstuvwxyz";
v.push_back(s); // copy
v.push_back(std::move(s)); // move; s is now unspecified
v.emplace_back(10, 'x'); // constructs "xxxxxxxxxx" directly
cout << "sizes: " << v[0].size() << ", "
<< v[1].size() << ", "
<< v[2].size() << "\n";
}
Output
sizes: 26, 0, 10
Prefer emplace_back(args…)
when you can construct directly. Prefer push_back(std::move(obj))
when you already have a fully constructed object you won’t use again.
Try it yourself
- Reserve capacity with
v.reserve(1000)
, then compare timings ofpush_back(s)
vspush_back(std::move(s))
for large strings. - Experiment with a custom class that logs copy/move to visualize container behavior.
🔹 Return by value, RVO, and when to std::move
Since C++17, returning a local by value uses guaranteed copy elision (NRVO/RVO), so neither copy nor move occurs. Do not std::move
a local variable at return; it can block elision and be slower.
#include <vector>
using namespace std;
vector<int> makeVec() {
vector<int> v(1'000'000, 7);
return v; // C++17: guaranteed elision
// return std::move(v); // DO NOT: may inhibit elision
}
Only std::move
when returning non-local references or when overloading forces it. For locals, let the compiler elide the return.
Try it yourself
- Create
makeBig()
returning a largestd::string
both with and withoutstd::move
. Inspect assembly or measure runtime in -O2; elision should win. - Return a member from a factory type by value; verify there’s no extra move under C++17.
🔹 std::move vs std::forward (quick comparison)
std::move
unconditionally casts to an rvalue. std::forward
conditionally casts based on a forwarding reference’s type parameter. Use forward
in templates that pass arguments through; use move
when you own the object and want to transfer it.
Feature | std::move | std::forward |
---|---|---|
Purpose | Cast to rvalue (enable moving) | Preserve value category in templates |
When to use | Transferring owned object | Forwarding function arguments |
Effect on lvalues | Makes rvalue | Keeps lvalue unless T is deduced as rvalue |
#include <utility>
#include <string>
void sink(const std::string&); // lvalue overload
void sink(std::string&&); // rvalue overload
template <typename T>
void relay(T&& x) {
sink(std::forward<T>(x)); // forwards as lvalue/rvalue properly
}
Try it yourself
- Add logging to each
sink
overload and callrelay(s)
andrelay(std::string("tmp"))
to see overload selection. - Replace
forward
withmove
and observe how you always call the rvalue overload (even for lvalues).
🔹 Common pitfalls with move in C++
- std::move doesn’t move by itself: It only casts; moving happens if a move overload exists.
- Avoid using moved-from objects: They’re valid but unspecified; reassign or destroy them.
- Don’t move from const: Moving from
const
usually falls back to copying, because move overloads typically require non-const rvalues. - Don’t std::move at return of locals: It may inhibit copy elision.
- Remember noexcept: Mark move operations
noexcept
so STL containers prefer them. - Be careful in operator=: Self-move is rare but possible; guard or use copy-and-swap.
// Example: self-move safe assignment via copy-and-swap
#include <utility>
class X {
public:
void swap(X& other) noexcept {/* swap members */}
X& operator=(X other) noexcept {
// by value: copy or move into 'other'
swap(other);
return *this;
}
};
Try it yourself
- Create a type with logging copy/move and test assignments using copy-and-swap. Confirm strong exception safety.
- Try to move from a
const
object and note that the copy constructor runs instead of the move constructor.
🔹 Mini project: Fast log buffer with move semantics
We will implement a small log entry type that is efficiently stored in a vector. Move semantics will avoid copies during growth and reordering.
#include <string>
#include <vector>
#include <iostream>
using namespace std;
struct LogEntry {
string level, msg;
LogEntry(string level, string msg)
: level(std::move(level)), msg(std::move(msg)) {}
LogEntry(const LogEntry&)
{ cout << "copy\n"; }
LogEntry(LogEntry&&) noexcept
{ cout << "move\n"; }
LogEntry& operator=(const LogEntry&)
{ cout << "copy assign\n"; return *this; }
LogEntry& operator=(LogEntry&&) noexcept
{ cout << "move assign\n"; return *this; }
};
int main() {
vector<LogEntry> logs;
logs.reserve(2); // reduce reallocations to highlight moves
logs.emplace_back("INFO", "Starting");
logs.emplace_back("WARN", "Low memory");
logs.emplace_back("ERROR", string(1000, 'X'));
// may reallocate and move existing entries
for (const auto& e : logs) {
cout << e.level << ": "
<< e.msg.substr(0, 10) << "...\n";
}
}
Output
move
INFO: Starting...
WARN: Low memor...
ERROR: XXXXXXXXXX...
Notice moves instead of copies when the vector grows. Construction also uses std::move
to initialize members efficiently from temporaries.
Try it yourself
- Remove
noexcept
from the move constructor and see if copies happen during reallocation (implementation-dependent). - Reserve different capacities and watch copy vs move counts.
🔹 Best practices for move in C++
- Use
std::move
when you are done with an object and want to transfer its resources. - Mark move constructor and move assignment
noexcept
whenever you can. - Don’t
std::move
local variables on return; let copy elision do its job. - Prefer
emplace_back
and perfect forwarding in factory helpers for efficient construction. - Do not move from objects you still need to use meaningfully.
- Expect moved-from objects to be valid but empty; design your types to support that invariant.
🔹 FAQs about move in C++
Q1: Does std::move actually move the object?
No. It casts to an rvalue. The move happens only if a move constructor/assignment exists for that type.
Q2: Is a moved-from object usable?
Yes, it must be valid but in an unspecified (often empty) state. You can assign to it, destroy it, or call operations that are documented to work on empty objects.
Q3: Why does moving from const often copy?
Move constructors typically take T&&
(non-const rvalue). If the object is const, moving would require modifying it (to empty it), which isn’t allowed, so the copy constructor is chosen.
Q4: Should I mark move operations noexcept?
Yes, if possible. Containers prefer moving only if move is noexcept
, otherwise they may fall back to copying.
Q5: When should I use std::move vs std::forward?
Use std::move
when transferring ownership of a known object. Use std::forward
inside template forwarding functions to preserve the input’s value category.
Q6: Do I ever need std::move on return?
Normally no for locals (C++17 guarantees elision). You might use it when returning a function parameter or a data member to select the rvalue overload.
Q7: Can I safely self-move in assignment?
It’s rare but possible. Implement move assignment to tolerate self-move (or use copy-and-swap idiom).
Q8: Does move always help performance?
Often, but measure. For small trivially copyable types (e.g., int
), copies are as cheap as moves.
Q9: Is std::move safe across threads?
Yes, but like any object access, you must ensure no data races. Moving doesn’t add synchronization.
Q10: Does move change the object’s address?
Moving the object itself doesn’t relocate it, but its resources may change (e.g., pointers inside). A moved-from container typically releases its buffer to the destination.
🔹 Wrapping up
move in C++ lets you transfer resources instead of copying, unlocking major performance improvements and enabling ownership-safe designs with types like std::unique_ptr
. Use std::move
when you’re done with an object, implement noexcept
move operations, prefer emplace
where possible, and let C++17 return value optimization do the heavy lifting on returns. Practice the “Try it yourself” tasks above to build intuition and write efficient modern C++.