Skip to content

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(x)). 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.

move_constructor.cpp
#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
}
Pitfall

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.

auto_generated_moves.cpp
#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");
}
Best Practice

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).

Key Takeaways
  • Moving steals resources (O(1)) instead of copying them (O(n))
  • std::move is just a cast to T&& — the actual move happens in the move constructor/assignment
  • Move constructors and move assignment must be noexcept for 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);`?