C++ Move Errors | Fixing 'use after move' Crashes and Move Semantics Mistakes
이 글의 핵심
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"
"Using the object after the move behaves strangely”
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'; // ❌ using a moved-from object → undefined behavior
This article covers:
- use-after-move bugs
- What std::move actually does
- Move constructors and move assignment
- Return value optimization (RVO)
- Ten common move-related mistakes
What production code looks like
When you learn from books, examples are tidy and theoretical. Production is different: legacy code, tight deadlines, and bugs you did not anticipate. The material here was learned in theory first; the important part is what you discover when you apply it in real projects and think, “So that is why the API is shaped this way.”
What sticks with me is the first project where I followed the book and still failed for days until a senior’s review showed the mistake. This guide covers not only the mechanics but also traps you are likely to hit in practice and how to fix them.
Table of contents
- What is std::move?
- The use-after-move bug
- Move constructor and move assignment
- Return value optimization (RVO)
- Ten common errors
- Summary
1. 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 an empty vector (valid but unspecified)
2. The use-after-move bug
Problematic code
// ❌ use after move
std::string str = "Hello";
std::string str2 = std::move(str);
std::cout << str << '\n'; // ❌ using a moved-from object
std::cout << str.size() << '\n'; // ❌ undefined behavior
After the move: the object is valid but unspecified (valid but unspecified state).
Generally safe:
- Destroying the object
- Reassigning it (
str = "World";) - Operations that reset it, such as
clear()orreset()where applicable
Risky:
- Reading state (
str.size(),str[0]) - Most member calls that assume non-empty contents (
str.append(), etc.)
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
3. 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.
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.
4. 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.
5. 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: missing move constructor
// ❌ No viable move, broken copy
class MyClass {
std::unique_ptr<int> ptr_;
public:
// Only user-declared copy constructor
MyClass(const MyClass& other) {
// unique_ptr is non-copyable → compile error
}
};
// ✅ Add move operations (often = default)
class MyClass {
std::unique_ptr<int> ptr_;
public:
MyClass(MyClass&& other) noexcept = default; // default move constructor
};
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: can delete then assign nullptr → crash
// ✅ Check for self-assignment
MyClass& operator=(MyClass&& other) noexcept {
if (this != &other) {
delete[] data_;
data_ = other.data_;
other.data_ = nullptr;
}
return *this;
}
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 using vec1)
for (int x : vec1) { // ❌ iterating a moved-from vector
// ...
}
// ✅ 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));
}
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
}
};
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)
- C++ move semantics — std::move guide
- C++ rvalue references (see the move-semantics series)
- C++ perfect forwarding — std::forward
- C++ Rule of Five — copy and move special members
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.
- Full C++ series
- C++ Adapter pattern
- C++ ADL
- C++ aggregate initialization