C++ Move Errors
Key takeaways
A practical guide to C++ move errors: use-after-move, how std::move works, move constructors and assignment, return-value optimization (RVO), and ten frequent mistakes—with fixes.
Introduction: “I used std::move and now it crashes”
C++11 move semantics remove unnecessary copies and improve performance, but misuse can crash your program or invoke undefined behavior.
// ❌ use after move
std::string str = "Hello";
std::string str2 = std::move(str); // move str's resources into str2
std::cout << str << '\n'; // ❌ prints an unspecified value (usually "", but not guaranteed)
That last line is a bug, but it is worth being precise about what kind of bug. For std::string it is not undefined behavior: the standard says a moved-from library object is in a “valid but unspecified state”, so printing it is legal and will print something—in practice an empty string with every major implementation. The bug is logical: the code reads a value that nobody is allowed to rely on. Undefined behavior shows up when a moved-from state has broken preconditions, for example dereferencing a moved-from std::unique_ptr (which is guaranteed to be null) or calling front() on a moved-from vector that is now empty. That distinction matters in practice, because the “harmless” string case tends to pass every test and only fails later when someone swaps the type for one with a different moved-from state.
The mistakes in this post come in three families. Some are about reading the source after a move (use-after-move). Some are about writing move operations incorrectly (missing noexcept, broken self-assignment, a user-declared copy constructor silently disabling the move). And some are about adding std::move where the compiler was already doing something better (returns, const objects). Recognizing which family a bug belongs to usually tells you where to look.
What is std::move?
std::move is only a cast
std::move does not move anything by itself. It only casts an lvalue to an rvalue (more precisely, to an xvalue) so overload resolution can pick a move operation when one exists.
std::string str = "Hello";
std::string str2 = std::move(str);
// ^^^^^^^^^^^
// lvalue → rvalue cast
// The actual move is done by the move constructor:
// std::string::string(std::string&& other)
Move vs copy
// Copy
std::vector<int> vec1(1000000, 42);
std::vector<int> vec2 = vec1; // copies one million elements (slow)
// Move
std::vector<int> vec3(1000000, 42);
std::vector<int> vec4 = std::move(vec3); // transfers internal pointer (fast)
// vec3 is now valid but unspecified (in practice: empty)
Because std::move is only a cast, whether anything actually moves depends entirely on what overload resolution finds. If the type has no move constructor—because it is a C++03-era class, because a member is not movable, or because a user-declared copy constructor or destructor suppressed the implicit move—then std::move(x) quietly selects the copy constructor instead. Nothing warns you. This is why “I added std::move and nothing got faster” is such a common complaint: the cast happened, but the type had nothing to move with. You can check with a static_assert(std::is_nothrow_move_constructible_v<MyType>) next to the class definition, which fails to compile the moment someone accidentally breaks the move.
The use-after-move bug
Problematic code
// ❌ use after move
std::string str = "Hello";
std::string str2 = std::move(str);
std::cout << str << '\n'; // ❌ value is unspecified
std::cout << str.size() << '\n'; // ❌ legal to call, but the result is meaningless
std::cout << str[0] << '\n'; // ❌ undefined behavior if str is now empty
After the move: the object is valid but unspecified (valid but unspecified state).
Safe:
- Destroying the object
- Reassigning it (
str = "World";) - Operations with no preconditions that put it into a known state, such as
clear(),reset(), orassign()
A bug (value is unspecified):
- Relying on what
str.size(),str.empty(), or iteration returns
Undefined behavior:
- Operations whose preconditions may now be violated:
str[0],vec.front(),*ptron a moved-fromunique_ptr
The guarantee differs by type, and one of them surprises almost everyone: a moved-from std::optional<T> still reports has_value() == true. Moving from an optional moves the contained T, it does not disengage the optional. Code that does auto v = std::move(opt); if (opt) { use(*opt); } therefore reads a moved-from T while the check “proves” it is present. std::unique_ptr and std::shared_ptr are the opposite: they are guaranteed to be null after a move, which is why they are the one case where testing the source after a move is meaningful.
The most insidious real-world form of use-after-move is hidden in a single expression. In process(std::move(name), name.size()), the order in which function arguments are evaluated is unspecified, so on some compilers name.size() runs after name has already been moved into the first parameter—and on others it does not. The same code can behave differently across GCC, Clang, and MSVC. Tools catch this reliably: clang-tidy’s bugprone-use-after-move check flags reads of a variable after it was passed to std::move, and it is worth enabling in CI for any codebase that uses moves heavily.
Fixes
// ✅ Reassign after the move
std::string str = "Hello";
std::string str2 = std::move(str);
str = "World"; // reassignment (safe)
std::cout << str << '\n'; // "World"
// ✅ Do not use the source after the move
std::string str3 = "Hello";
std::string str4 = std::move(str3);
// do not read str3
Move constructor and move assignment
Implementing a move constructor
class MyClass {
int* data_;
size_t size_;
public:
// Move constructor
MyClass(MyClass&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr; // leave source empty
other.size_ = 0;
}
// Move assignment operator
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
delete[] data_; // release existing resources
data_ = other.data_;
size_ = other.size_;
other.data_ = nullptr;
other.size_ = 0;
}
return *this;
}
~MyClass() {
delete[] data_;
}
};
Note: the noexcept specifier matters for STL containers and some optimizations.
The reason is concrete. When a std::vector grows, it allocates a new buffer and has to transfer every element. If a move constructor could throw halfway through, the vector could not roll back—the old buffer would already have been partially moved out of—so it would lose its strong exception guarantee. std::vector therefore uses std::move_if_noexcept: it moves elements only if the move constructor is noexcept (or the type is not copyable at all), and copies them otherwise. Forgetting noexcept on a hand-written move constructor produces no error and no warning; it just makes every reallocation copy all elements. For a vector of large objects this can quietly turn an O(n) amortized append into a much more expensive operation. Also note that the move constructor above sets other.size_ = 0 along with the pointer. Leaving size_ unchanged would leave the source in a state where size_ claims elements exist while data_ is null, which is exactly the kind of inconsistent moved-from state that turns a logic bug into a crash.
Rule of Five
class MyClass {
public:
// 1. Destructor
~MyClass();
// 2. Copy constructor
MyClass(const MyClass& other);
// 3. Copy assignment operator
MyClass& operator=(const MyClass& other);
// 4. Move constructor
MyClass(MyClass&& other) noexcept;
// 5. Move assignment operator
MyClass& operator=(MyClass&& other) noexcept;
};
Rule of thumb: if you customize one of these five, you should consider all five together.
The trap here is that the compiler stops generating move operations as soon as you declare a destructor, a copy constructor, or a copy assignment operator—even ~MyClass() = default; or a destructor that only logs. Such a class is still copyable, so every std::move of it compiles and silently copies. A common way this happens in real code is adding a destructor “just to put a breakpoint in it” or to log object lifetimes, after which a container of those objects gets noticeably slower and nobody connects the two changes. The cleanest way out is the Rule of Zero: let members such as std::vector, std::string, and std::unique_ptr own resources, declare none of the five, and the compiler generates correct, noexcept moves for you. When you do need a custom destructor, explicitly default the moves (MyClass(MyClass&&) noexcept = default;) so they are not lost.
Return value optimization (RVO)
What is RVO?
RVO (return value optimization) lets the compiler elide copy and move operations by constructing the return value directly in the caller’s storage.
// Neither copy nor move (RVO)
std::vector<int> createVector() {
std::vector<int> vec(1000000, 42);
return vec; // RVO: no copy/move of the vector object
}
std::vector<int> result = createVector(); // constructed in place
When not to use std::move on a return
// ❌ Blocks RVO
std::vector<int> createVector() {
std::vector<int> vec(1000000, 42);
return std::move(vec); // ❌ can inhibit RVO → forces a move
}
// ✅ Preferred
std::vector<int> createVector() {
std::vector<int> vec(1000000, 42);
return vec; // RVO
}
Rule of thumb: do not std::move a local variable you are returning by value unless you have a specific, measured reason.
It helps to separate two cases. Returning a prvalue (return std::vector<int>(n);) has guaranteed copy elision since C++17—no copy or move constructor is even required to exist. Returning a named local (return vec;) is NRVO, which is permitted but not guaranteed; however, if the compiler does not elide, the language still treats the returned local as an rvalue and moves it automatically. So plain return vec; is never worse than return std::move(vec);, and it is often better, because writing std::move(vec) turns the expression into something other than a plain name and disqualifies NRVO. GCC warns about this with -Wpessimizing-move (enabled by -Wall), and Clang has the same diagnostic. Where std::move on return is needed is when you return a member or a function parameter’s sub-object that the compiler cannot treat as a local, such as return std::move(this->buffer_); in a function that deliberately hands off a member.
Ten common errors
Error 1: use after move
// ❌ Use after move
std::unique_ptr<int> ptr1 = std::make_unique<int>(42);
std::unique_ptr<int> ptr2 = std::move(ptr1);
std::cout << *ptr1 << '\n'; // ❌ dereferencing nullptr → crash
Error 2: moving a const object
// ❌ const cannot be moved from
const std::string str = "Hello";
std::string str2 = std::move(str); // not a move—a copy!
// std::move yields const T&&, but the move constructor takes T&& (non-const),
// so overload resolution picks the copy constructor
Error 3: std::move on a return value
// ❌ Hurts RVO
std::vector<int> foo() {
std::vector<int> vec = {1, 2, 3};
return std::move(vec); // ❌ unnecessary / harmful
}
// ✅ Preferred
std::vector<int> foo() {
std::vector<int> vec = {1, 2, 3};
return vec; // RVO
}
Error 4: a user-declared copy constructor silently disables the move
// ❌ Compiles, but "moving" actually calls this copy constructor
class MyClass {
std::unique_ptr<int> ptr_;
public:
MyClass() : ptr_(std::make_unique<int>(42)) {}
// User-declared copy constructor: suppresses the implicit move constructor
MyClass(const MyClass& other) {
// ptr_ is left null here (unique_ptr cannot be copied)
}
};
MyClass a;
MyClass b = std::move(a); // selects the copy constructor → b.ptr_ is null, a still owns the int
// ✅ Declare the move operations explicitly (often = default)
class MyClass2 {
std::unique_ptr<int> ptr_;
public:
MyClass2() = default;
MyClass2(MyClass2&& other) noexcept = default;
MyClass2& operator=(MyClass2&& other) noexcept = default;
};
This is one of the nastier move bugs because there is no compile error: an rvalue binds happily to const MyClass&, so overload resolution picks the copy constructor. The result is a “moved” object that did not receive the resource, and a source object that still holds it. If the copy constructor had tried to copy ptr_ directly, you would at least get a compile error—the silent version appears when someone writes a partial copy constructor, as above. Declaring the moves explicitly, or removing the hand-written copy constructor entirely, fixes it.
Error 5: self-move assignment without a guard
// ❌ No self-assignment check
MyClass& operator=(MyClass&& other) noexcept {
delete[] data_;
data_ = other.data_;
other.data_ = nullptr;
return *this;
}
MyClass obj;
obj = std::move(obj); // self-move: frees data_, then sets it to nullptr → contents silently lost
// ✅ Check for self-assignment
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
other.data_ = nullptr;
}
return *this;
}
Nobody writes obj = std::move(obj) on purpose, but self-move happens indirectly: std::swap(a, a), algorithms like std::remove_if that move elements within the same range, or v[i] = std::move(v[j]) when i == j. Tracing the unguarded version shows why it is dangerous: delete[] data_ frees the buffer, data_ = other.data_ copies the now-dangling pointer back into itself, and other.data_ = nullptr then nulls it—so the object ends up empty and its contents are gone without any error. A variant that updates size before nulling can leave size_ non-zero with a null pointer, which crashes on the next access. An alternative to the explicit check is the move-and-swap idiom: take the argument by value (MyClass& operator=(MyClass other) noexcept { swap(*this, other); return *this; }), which handles self-assignment naturally at the cost of one extra move.
Error 6: std::move on a function argument (context-dependent)
Example with process:
// Sometimes redundant std::move
void process(std::string s) { // pass by value: move or copy into s
// ...
}
std::string str = "Hello";
process(std::move(str)); // explicit move from str (often what you want if str dies here)
// If you still need str afterward:
process(str); // copy
// If the callee should not take ownership by value, prefer const string& or string_view
Error 7: returning an rvalue reference to a local
// ❌ Returning a reference to a local
std::string&& foo() {
std::string str = "Hello";
return std::move(str); // ❌ dangling reference after return
}
// ✅ Return by value
std::string foo() {
std::string str = "Hello";
return str; // RVO
}
Error 8: non-movable type
// ❌ Move deleted
class NonMovable {
public:
NonMovable(NonMovable&&) = delete; // deleted move constructor
};
NonMovable obj1;
NonMovable obj2 = std::move(obj1); // compile error
// error: use of deleted function 'NonMovable::NonMovable(NonMovable&&)'
Error 9: assuming vector size after move
// ❌ Relying on moved-from vector state
std::vector<int> vec1(1000, 42);
std::vector<int> vec2 = std::move(vec1);
// vec1.size() is unspecified (typically 0, but do not rely on it)
for (int x : vec1) { // ❌ legal, but iterates an unspecified number of elements
// ...
}
// ✅ Treat moved-from object as empty or unused
std::vector<int> vec3(1000, 42);
std::vector<int> vec4 = std::move(vec3);
// do not use vec3 except to destroy or reassign
Error 10: perfect forwarding mistake
Example wrapper:
// ❌ Rvalue becomes lvalue inside the function
template <typename T>
void wrapper(T&& arg) {
process(arg); // ❌ arg has a name → lvalue
}
// ✅ std::forward preserves value category
template <typename T>
void wrapper(T&& arg) {
process(std::forward<T>(arg));
}
The opposite mistake is just as common: using std::move on a forwarding reference. Inside template <typename T> void wrapper(T&& arg), T&& can bind to an lvalue the caller still owns. Writing process(std::move(arg)) would move out of the caller’s variable even though they passed it as an lvalue and expect it untouched. The rule is simple: std::move for rvalue references to concrete types (std::string&&), std::forward<T> for forwarding references (T&& where T is deduced). Also forward only once—calling std::forward<T>(arg) twice in the same function can move from arg twice.
Real-world patterns
Case 1: vector return (already optimal)
// ❌ Unnecessary worry about copy (RVO applies)
std::vector<int> createData() {
std::vector<int> data(1000000, 42);
return data; // RVO: no copy of the vector object
}
void process() {
std::vector<int> result = createData(); // RVO
}
After: no change needed—std::move on the return is not required.
Case 2: transferring unique_ptr ownership
class ResourceManager {
std::vector<std::unique_ptr<Resource>> resources_;
public:
void add(std::unique_ptr<Resource> res) {
resources_.push_back(std::move(res)); // transfer ownership
}
std::unique_ptr<Resource> take(size_t idx) {
auto res = std::move(resources_[idx]); // transfer out of slot
resources_.erase(resources_.begin() + idx);
return res; // RVO; std::move on return not needed
}
};
Note the order in take: the element is moved out first, then its now-null slot is erased. Doing it the other way round—erasing first—would destroy the resource before you had a chance to take it. And because add takes the unique_ptr by value, callers must write mgr.add(std::move(ptr)) or pass a temporary like mgr.add(std::make_unique<Resource>()); mgr.add(ptr) fails to compile with “use of deleted function unique_ptr(const unique_ptr&)”, which is exactly the point—ownership transfer becomes visible at the call site.
Case 3: move-only type
// Copy deleted; move allowed
class MoveOnly {
std::unique_ptr<int> data_;
public:
MoveOnly() = default;
MoveOnly(const MoveOnly&) = delete;
MoveOnly& operator=(const MoveOnly&) = delete;
MoveOnly(MoveOnly&&) noexcept = default;
MoveOnly& operator=(MoveOnly&&) noexcept = default;
};
MoveOnly obj1;
MoveOnly obj2 = std::move(obj1); // OK
// MoveOnly obj3 = obj1; // compile error
Summary
Move-safety checklist
- Do I avoid using objects after
std::moveexcept to destroy or reassign? - Do I avoid unnecessary
std::moveon returned locals? - Are move constructors
noexceptwhere appropriate? - Do I avoid expecting a move from
constobjects? - Does move assignment handle self-assignment?
When to use std::move (rules of thumb)
| Situation | std::move? | Why |
|---|---|---|
| Return local by value | No (usually) | RVO |
| Transfer ownership | Yes | unique_ptr, containers, etc. |
| Push into vector | Often yes | Avoid copying large objects |
| Pass by value (sink) | Optional | Makes transfer explicit |
const object | Pointless | You get a copy |
Core rules
- std::move is a cast; the move operation runs in a constructor or assignment operator.
- Do not read a moved-from object until you reassign it or otherwise put it in a known state.
- Avoid
std::moveon returned locals unless you know you are blocking RVO on purpose. - Prefer
noexceptmove operations for types stored in standard containers. - Follow the Rule of Five when you manage resources manually.
Related posts (on this site)
Closing
Move semantics are central to modern C++, but misuse leads to crashes and undefined behavior.
Principles:
- Do not use moved-from objects for ordinary reads or operations until reassigned.
- Do not
std::movereturned locals without a good reason (RVO). - Use
noexcepton moves when appropriate for your type. - Apply the Rule of Five when you own raw resources.
std::move is a key tool for performance, but do not sprinkle it everywhere—the compiler often optimizes returns and copies without explicit moves.
Next steps: after move semantics, deepen your understanding with perfect forwarding and rvalue-reference material in the C++ series.
Frequently Asked Questions (FAQ)
Q. Why does std::vector copy my objects when it grows instead of moving them?
A. During reallocation vector moves elements only if the move constructor is noexcept (or the type cannot be copied), because it has to preserve the strong exception guarantee; otherwise it falls back to copying. Mark your move constructor and move assignment noexcept. Defaulted move operations are noexcept automatically when all members’ move operations are.
Related Articles
- C++ RVO and NRVO: Copy Elision and Why return std::move(local) Backfires
- C++ Move Semantics: Copy vs Move Explained
- C++ Move Constructor: rvalue Stealing & noexcept Best