C++ Move Errors | Fixing 'use after move' Crashes and Move Semantics Mistakes

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

  1. What is std::move?
  2. The use-after-move bug
  3. Move constructor and move assignment
  4. Return value optimization (RVO)
  5. Ten common errors
  6. 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() or reset() 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::move except to destroy or reassign?
  • Do I avoid unnecessary std::move on returned locals?
  • Are move constructors noexcept where appropriate?
  • Do I avoid expecting a move from const objects?
  • Does move assignment handle self-assignment?

When to use std::move (rules of thumb)

Situationstd::move?Why
Return local by valueNo (usually)RVO
Transfer ownershipYesunique_ptr, containers, etc.
Push into vectorOften yesAvoid copying large objects
Pass by value (sink)OptionalMakes transfer explicit
const objectPointlessYou get a copy

Core rules

  1. std::move is a cast; the move operation runs in a constructor or assignment operator.
  2. Do not read a moved-from object until you reassign it or otherwise put it in a known state.
  3. Avoid std::move on returned locals unless you know you are blocking RVO on purpose.
  4. Prefer noexcept move operations for types stored in standard containers.
  5. Follow the Rule of Five when you manage resources manually.


Closing

Move semantics are central to modern C++, but misuse leads to crashes and undefined behavior.

Principles:

  1. Do not use moved-from objects for ordinary reads or operations until reassigned.
  2. Do not std::move returned locals without a good reason (RVO).
  3. Use noexcept on moves when appropriate for your type.
  4. 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.


More on this blog