C++ move semantics rvalue references std::move tutorial
|

C++ Move Semantics: Rvalue References, std::move & Perfect Forwarding 2026

1. The Copy Problem

Before C++11, returning a large object from a function meant copying all its data — even when the original was about to be destroyed:

std::vector<int> createBigVector() {
    std::vector<int> v(1'000'000); // 1 million ints
    // ... fill v ...
    return v; // Pre-C++11: copies 4MB of data!
}

Move semantics solve this by transferring ownership of resources instead of copying them. It’s like giving someone your house keys instead of building them an identical house.

2. Lvalues vs Rvalues

Understanding value categories is essential for move semantics:

int x = 42;        // x is an lvalue (has a name, persistent address)
int y = x + 1;     // (x + 1) is an rvalue (temporary, no persistent address)

std::string s = "hello";
std::string t = s + " world"; // (s + " world") is an rvalue

// Lvalues: variables, array elements, dereferenced pointers, references
// Rvalues: temporaries, literals, return values from functions

// You can take the address of an lvalue, not an rvalue
int* p = &x;    // OK: x is an lvalue
// int* q = &42; // Error: 42 is an rvalue

3. Rvalue References (&&)

C++11 introduced rvalue references (T&&) that can bind to temporaries:

#include <iostream>
#include <string>

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

void process(std::string&& s) {
    std::cout << "rvalue: " << s << "\n";
    // We can safely steal s's resources here
    // because the caller doesn't need them anymore
}

int main() {
    std::string name = "Alice";

    process(name);                    // calls lvalue overload
    process("Bob");                   // calls rvalue overload
    process(name + " Smith");         // calls rvalue overload
    process(std::move(name));         // calls rvalue overload (see section 5)
    // name is now in a valid-but-unspecified state
}

4. Move Constructor & Move Assignment

A move constructor steals resources from a dying object instead of copying them:

#include <iostream>
#include <cstring>
#include <utility>

class Buffer {
    char* data_;
    size_t size_;

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

    // Copy constructor (expensive)
    Buffer(const Buffer& other)
        : data_(new char[other.size_]), size_(other.size_) {
        std::memcpy(data_, other.data_, size_);
        std::cout << "Copied " << size_ << " bytes\n";
    }

    // Move constructor (cheap — just pointer swap)
    Buffer(Buffer&& other) noexcept
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;  // Leave source in valid state
        other.size_ = 0;
        std::cout << "Moved " << size_ << " bytes\n";
    }

    // Copy assignment
    Buffer& operator=(const Buffer& other) {
        if (this != &other) {
            delete[] data_;
            data_ = new char[other.size_];
            size_ = other.size_;
            std::memcpy(data_, other.data_, size_);
        }
        return *this;
    }

    // Move assignment
    Buffer& operator=(Buffer&& other) noexcept {
        if (this != &other) {
            delete[] data_;
            data_ = other.data_;
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }

    ~Buffer() { delete[] data_; }

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

int main() {
    Buffer b1(1024);          // Constructed 1024 bytes
    Buffer b2 = b1;           // Copied 1024 bytes
    Buffer b3 = std::move(b1); // Moved 1024 bytes (fast!)
    // b1 is now empty (size 0, null data)
}
Critical: Always mark move constructors and move assignment operators noexcept. STL containers like std::vector will only use moves during reallocation if they’re noexcept — otherwise they fall back to copying for exception safety.

5. std::move — Casting to Rvalue

std::move doesn’t actually move anything — it’s just a cast to an rvalue reference, signaling “I’m done with this, you can steal its guts”:

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

int main() {
    std::string source = "Hello, World!";

    // std::move casts source to std::string&&
    std::string dest = std::move(source);
    // source is now empty (moved-from state)

    std::cout << "dest: " << dest << "\n";     // "Hello, World!"
    std::cout << "source: " << source << "\n"; // "" (empty)

    // Moving into containers
    std::vector<std::string> words;
    std::string word = "efficiency";
    words.push_back(word);            // copies word
    words.push_back(std::move(word)); // moves word (faster)
    // word is now empty

    // emplace_back constructs in-place (even better)
    words.emplace_back("constructed directly");

    // Moving from containers
    std::string extracted = std::move(words[0]);
    // words[0] is now empty but vector still has 3 elements
}

6. Rule of Five Revisited

If you need any of these, you probably need all five (see Rule of 3/5/0):

class Resource {
public:
    Resource();                                    // Constructor
    ~Resource();                                   // 1. Destructor
    Resource(const Resource&);                     // 2. Copy constructor
    Resource& operator=(const Resource&);          // 3. Copy assignment
    Resource(Resource&&) noexcept;                 // 4. Move constructor
    Resource& operator=(Resource&&) noexcept;      // 5. Move assignment
};

// Rule of Zero: prefer no custom special members
// Use smart pointers and RAII containers
class ModernResource {
    std::unique_ptr<Data> data_;
    std::vector<int> cache_;
    // Compiler generates correct move/copy/destroy automatically!
};

7. Perfect Forwarding

Perfect forwarding preserves the value category (lvalue/rvalue) of arguments through template functions:

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

void process(const std::string& s) { std::cout << "lvalue ref\n"; }
void process(std::string&& s)      { std::cout << "rvalue ref\n"; }

// Without forwarding: always calls lvalue version
template<typename T>
void wrapperBad(T&& arg) {
    process(arg); // arg is named → always an lvalue!
}

// With forwarding: preserves original value category
template<typename T>
void wrapperGood(T&& arg) {
    process(std::forward<T>(arg)); // forwards as lvalue or rvalue
}

// Factory pattern with perfect forwarding
template<typename T, typename... Args>
std::unique_ptr<T> make_unique_custom(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

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

    wrapperBad(s);              // lvalue ref ✓
    wrapperBad(std::string("temp")); // lvalue ref ✗ (should be rvalue)

    wrapperGood(s);              // lvalue ref ✓
    wrapperGood(std::string("temp")); // rvalue ref ✓

    auto p = make_unique_custom<std::string>("forwarded!");
    std::cout << *p << std::endl;
}
Forwarding Reference Rule: T&& is only a forwarding (universal) reference when T is deduced. std::string&& is always an rvalue reference, but auto&& and T&& in templates are forwarding references.

8. Real-World Patterns

Sink Parameters

class Logger {
    std::vector<std::string> logs_;

public:
    // Take by value and move into storage
    void addLog(std::string message) {
        logs_.push_back(std::move(message));
    }
    // Called with lvalue: 1 copy + 1 move
    // Called with rvalue: 1 move + 1 move
    // Called with literal: 1 construction + 1 move
};

Return Value Optimization (RVO)

std::vector<int> generateData() {
    std::vector<int> result;
    result.reserve(1000);
    for (int i = 0; i < 1000; ++i)
        result.push_back(i);
    return result; // RVO: no copy, no move — constructed in place
    // Don't write: return std::move(result); — this PREVENTS RVO!
}

Moving Unique Pointers

#include <memory>

class GameEngine {
    std::vector<std::unique_ptr<Entity>> entities_;

public:
    void addEntity(std::unique_ptr<Entity> entity) {
        entities_.push_back(std::move(entity)); // Must move — can't copy
    }

    std::unique_ptr<Entity> removeEntity(int index) {
        auto entity = std::move(entities_[index]);
        entities_.erase(entities_.begin() + index);
        return entity; // RVO applies
    }
};

9. Practice Exercises

Exercise 1: Move-Aware String

Implement a simple string class with move constructor and move assignment. Count copies vs moves to verify efficiency.

Exercise 2: Object Pool

Create an object pool that uses std::move to return objects and accept them back without copying.

Exercise 3: Perfect-Forwarding Wrapper

Write a timed_call function that measures execution time of any callable with any arguments, using perfect forwarding.

What’s Next?

With move semantics mastered, you’re ready for constexpr & Compile-Time Computing — pushing computation from runtime to compile time for maximum performance.

Return to the C++ Learning Roadmap to continue your journey.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *