Skip to content

Move Semantics — Rvalue References, Move Constructors, Move Assignment, std::move

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Move Semantics. We cover key concepts, practical examples, and best practices to help you master this topic.

C++ move semantics enable transferring ownership of resources (heap memory, file handles, sockets) from temporary objects to permanent ones, replacing expensive deep copies with cheap pointer swaps.

What You'll Learn

You will understand rvalue references and value categories (lvalue, prvalue, xvalue), write move constructors and move assignment operators, use std::move to cast to rvalue references, implement the rule of five for resource-managing classes, and apply move semantics in STL containers, std::unique_ptr, and performance-critical code.

Why It Matters

Move semantics eliminate the biggest performance problem in C++98: unnecessary copies. Returning a large std::vector from a function used to copy all elements; now it moves them in O(1). Every modern C++ library — including the STL — relies on move semantics for performance. Understanding moves is essential for writing efficient, modern C++.

Learning Path

graph LR
    A["51: auto & decltype"] --> B["52: Move Semantics"]
    B --> C["53: Perfect Forwarding"]
    C --> D["54: Structured Bindings"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

Value Categories

Every expression in C++ is one of three value categories:

  • lvalue: has an identity, address can be taken (variables, references)
  • prvalue (pure rvalue): temporary, no identity (literals, function returns)
  • xvalue (eXpiring value): about to expire, resources can be stolen (result of std::move)
#include <iostream>
#include <string>
#include <utility>

void category(const std::string&) {
    std::cout << "lvalue reference\n";
}

void category(std::string&&) {
    std::cout << "rvalue reference\n";
}

int main() {
    std::string s = "hello";

    category(s);             // lvalue reference (s is an lvalue)
    category("world");       // rvalue reference (string literal → temporary)
    category(std::move(s));  // rvalue reference (cast to xvalue)
    // After move: s is valid but unspecified (typically empty)
    std::cout << "s after move: '" << s << "'\n";
}

Rvalue References (T&&)

Rvalue references bind only to rvalues — temporaries and expiring values.

#include <iostream>
#include <string>

void process(int& x) {
    std::cout << "Lvalue: " << x << "\n";
}

void process(int&& x) {
    std::cout << "Rvalue: " << x << "\n";
}

int main() {
    int a = 10;

    process(a);              // Lvalue
    process(20);             // Rvalue
    process(std::move(a));   // Rvalue (cast to xvalue)
    process(a + 5);          // Rvalue (temporary result)
}

Move Constructor and Move Assignment

The move constructor "steals" resources from a temporary object, leaving it in a valid-but-empty state.

#include <iostream>
#include <cstring>
#include <algorithm>

class Buffer {
    char* data_;
    size_t size_;

public:
    // Constructor
    Buffer(size_t size) : data_(new char[size]), size_(size) {
        std::fill(data_, data_ + size_, 0);
        std::cout << "Constructed, size=" << size_ << "\n";
    }

    // Destructor
    ~Buffer() {
        delete[] data_;
        std::cout << "Destroyed, size=" << size_ << "\n";
    }

    // Copy constructor (deep copy)
    Buffer(const Buffer& other) : data_(new char[other.size_]), size_(other.size_) {
        std::copy(other.data_, other.data_ + size_, data_);
        std::cout << "Copy constructed, size=" << size_ << "\n";
    }

    // Move constructor (steal resources)
    Buffer(Buffer&& other) noexcept
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;
        other.size_ = 0;
        std::cout << "Move constructed, size=" << size_ << "\n";
    }

    // Move assignment
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
            std::cout << "Move assigned, size=" << size_ << "\n";
        }
        return *this;
    }

    size_t size() const { return size_; }
};

int main() {
    Buffer b1(100);                // Constructed

    Buffer b2 = b1;                // Copy constructed
    std::cout << "b2 size: " << b2.size() << "\n";  // 100

    Buffer b3 = std::move(b1);     // Move constructed
    std::cout << "b1 size: " << b1.size() << "\n";  // 0 (moved-from)
    std::cout << "b3 size: " << b3.size() << "\n";  // 100

    b2 = std::move(b3);            // Move assigned
    std::cout << "b3 size: " << b3.size() << "\n";  // 0 (moved-from)
}

The Rule of Five

If a class manages a resource (raw pointer, handle, etc.), you should implement all five special member functions:

class Resource {
public:
    // 1. Destructor
    ~Resource();

    // 2. Copy constructor
    Resource(const Resource&);

    // 3. Copy assignment
    Resource& operator=(const Resource&);

    // 4. Move constructor
    Resource(Resource&&) noexcept;

    // 5. Move assignment
    Resource& operator=(Resource&&) noexcept;
};

If you implement any of these, you likely need all five. For classes that don't manage resources directly (members handle their own cleanup), follow the Rule of Zero: define none of them.

std::move — What It Actually Does

std::move(x) does NOT move anything. It just casts x to an rvalue reference (xvalue) so that move operations can be selected.

#include <iostream>
#include <string>
#include <utility>

int main() {
    std::string a = "Hello, this is a long string";
    std::string b;

    // std::move just casts to T&& — no data movement here
    std::cout << "Before move: a = '" << a << "'\n";

    // The actual movement happens in the move assignment operator
    b = std::move(a);

    std::cout << "After move: a = '" << a << "'\n";  // empty or unspecified
    std::cout << "After move: b = '" << b << "'\n";  // "Hello, this is a long string"
}

After std::move, the source object is in a valid but unspecified state. It must be destructible and assignable, but nothing else is guaranteed.

Move Semantics with STL

STL containers are move-aware, making operations like returning large containers efficient.

#include <iostream>
#include <vector>
#include <string>

// Building a large vector
std::vector<int> buildLargeVector(size_t n) {
    std::vector<int> v;
    v.reserve(n);
    for (size_t i = 0; i < n; ++i) {
        v.push_back(static_cast<int>(i));
    }
    // In C++11: this return uses move semantics (no copy!)
    return v;
}

int main() {
    // Construct in place (no copy)
    std::vector<std::string> strings;
    strings.emplace_back("Constructed in place, no temporary");

    // Inserting with std::move
    std::string temp = "Moving this into the vector";
    strings.push_back(std::move(temp));
    // temp is now empty

    // Returning large objects from functions
    auto big = buildLargeVector(1000000);  // O(1) move, not O(n) copy
    std::cout << "Size: " << big.size() << "\n";  // 1000000

    // Swapping is efficient (moves, not copies)
    std::vector<int> v1(100), v2(200);
    v1.swap(v2);  // swaps internal pointers, O(1)
    std::cout << "v1 size: " << v1.size() << "\n";  // 200
}

Move-Only Types

Some types cannot be copied but can be moved.

#include <iostream>
#include <memory>
#include <vector>

int main() {
    // std::unique_ptr is move-only
    auto ptr = std::make_unique<int>(42);

    // auto copy = ptr;  // Error: unique_ptr is not copyable

    auto moved = std::move(ptr);   // OK: transfers ownership
    // ptr is now null

    std::cout << "moved: " << *moved << "\n";  // 42
    std::cout << "ptr is null: " << (ptr == nullptr) << "\n";  // 1

    // Store move-only types in containers
    std::vector<std::unique_ptr<int>> vec;
    vec.push_back(std::make_unique<int>(1));
    vec.push_back(std::make_unique<int>(2));

    auto extracted = std::move(vec[0]);  // Move unique_ptr out of vector
    std::cout << "extracted: " << *extracted << "\n";  // 1
}

When Moves Happen Automatically

The compiler generates move operations automatically in many cases.

#include <iostream>
#include <string>
#include <vector>

struct Person {
    std::string name;
    int age;

    // Compiler-generated move constructor/assignment since all members are movable
};

int main() {
    Person p1{"Alice", 30};
    Person p2 = std::move(p1);  // Uses implicitly-declared move constructor

    // Automatic move in return
    auto makePerson = []() -> Person {
        Person p{"Bob", 25};
        return p;  // Move or NRVO (named return value optimization)
    };

    Person p3 = makePerson();

    // Automatic move when passing temporary
    std::vector<std::string> v;
    v.push_back("temporary");  // Move (temporary is rvalue)
    // v.push_back(s);        // Copy (s is lvalue)
    // v.push_back(std::move(s));  // Move (cast to rvalue)
}

Common Mistakes

Mistake 1: Using moved-from objects

std::string s = "hello";
std::vector<std::string> v;
v.push_back(std::move(s));
std::cout << s.size();  // Probably 0, but unspecified. Don't do this!

Only destroy or reassign moved-from objects. Do not assume their state.

Mistake 2: Forgetting noexcept on move operations

class Bad {
public:
    Bad(Bad&& other) /* missing noexcept */ {
        // STL containers prefer copy over throwing move for strong exception guarantee
    }
};

Always mark move constructors and move assignment noexcept so STL containers use them.

Mistake 3: Move instead of forward

template <typename T>
void wrapper(T&& arg) {
    // Wrong: arg is always an lvalue (has a name)
    target(std::move(arg));  // Forces move even for lvalue inputs

    // Correct: preserves value category
    target(std::forward<T>(arg));
}

Use std::forward for forwarding references, not std::move.

Mistake 4: Self-move assignment not checked

Buffer& operator=(Buffer&& other) noexcept {
    // Without self-check, we'd delete our own data then steal it
    if (this != &other) { ... }
    return *this;
}

Mistake 5: Returning std::move from a function

std::vector<int> create() {
    std::vector<int> v = {1, 2, 3};
    return std::move(v);  // Anti-pattern! Prevents NRVO
    // Just: return v;  // Compiler will use move or NRVO
}

return v; already uses move semantics. Adding std::move inhibits copy elision.

Practice Questions

  1. What is the output?
std::vector<int> v(100);
auto v2 = std::move(v);
std::cout << v.size();

Answer: 0 — v is moved-from, its size is now 0 (or unspecified, but typically 0).

  1. What does std::move actually do? Answer: It casts its argument to an rvalue reference (xvalue). No data movement happens until a move constructor/assignment is called.

  2. When should you mark move operations noexcept? Answer: Always. STL containers use std::move_if_noexcept and prefer copy over a potentially throwing move.

  3. What is the Rule of Five? Answer: If you implement any of destructor, copy constructor, copy assignment, move constructor, or move assignment, you typically need all five.

  4. Why is return v; better than return std::move(v);? Answer: return v; allows copy elision (NRVO). return std::move(v); forces move and prevents elision.

FAQ

What are move semantics in C++

Move semantics transfer ownership of resources from temporary objects using rvalue references. They replace expensive deep copies with cheap pointer swaps for performance.

What is the difference between std::move and std::forward

std::move unconditionally casts to rvalue reference. std::forward conditionally casts based on whether the original argument was an rvalue. Use forward for forwarding references.

What happens to a moved-from object

It is left in a valid but unspecified state. You can destroy it, assign to it, or call member functions with no preconditions. Do not assume it still holds its original value.

Why do STL containers need noexcept move constructors

Containers use strong exception safety: if a reallocation throws, the original state must be preserved. If move throws, they fall back to copy.

When does the compiler automatically generate move operations

When no copy operations, move operations, or destructor are user-declared. Implicitly declared move operations are noexcept and do member-wise moves.

Mini Project

Implement a move-only FileDescriptor RAII wrapper that manages a POSIX file descriptor:

#include <iostream>
#include <utility>
#include <unistd.h>
#include <fcntl.h>

// Your FileDescriptor class with move semantics

int main() {
    // Open a file
    FileDescriptor fd1(open("test.txt", O_CREAT | O_WRONLY, 0644));
    if (fd1) {
        write(fd1.get(), "Hello\n", 6);
    }

    // Transfer ownership
    FileDescriptor fd2 = std::move(fd1);
    // fd1 is now closed (moved-from)

    // fd1 is no longer valid
    // write(fd1.get(), "data", 4);  // Would fail: fd1 is closed

    // fd2 automatically closes on destruction
}

This project mirrors how C++ standard library handles resource ownership — std::unique_ptr, std::ifstream, and many other RAII types use move semantics. Compare with Java where all objects are reference types and moves are unnecessary.

What's Next

You now understand move semantics — the foundation of efficient C++ resource management. Next, you will learn perfect forwarding, which preserves value categories through template functions using std::forward.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro