Move Semantics Deep Dive

Understanding rvalue references, perfect forwarding, and move operations

The Performance Problem

Before C++11, returning large objects or passing them to functions required expensive copies.

// Pre-C++11: Expensive copy
std::vector<int> create_large_vector() {
    std::vector<int> result(1000000);
    // fill data...
    return result;  // Copy! (or rely on RVO)
}

void process(std::vector<int> data);  // Takes by value

std::vector<int> v = create_large_vector();  // Copy!
process(v);  // Another copy!

Value Categories

CategoryCan be moved from?Example
lvalueNo (has identity, can be addressed)int x; x
prvalue ("pure" rvalue)Yes42, func()
xvalue ("expiring" value)Yesstd::move(x)
rvalue (prvalue + xvalue)YesTemporary or moved values

Rvalue References

int x = 42;
int& lref = x;          // lvalue reference (binds to lvalues only)
int&& rref = 42;        // rvalue reference (binds to rvalues only)
int&& rref2 = std::move(x);  // xvalue is also an rvalue

// Function overload resolution
void process(int& value);   // #1: lvalue reference
void process(int&& value);  // #2: rvalue reference

int a = 42;
process(a);              // Calls #1
process(42);             // Calls #2
process(std::move(a));   // Calls #2

Move Semantics in Practice

1. Move Constructor

class Buffer {
    size_t size_;
    char* data_;
    
public:
    // Copy constructor (expensive)
    Buffer(const Buffer& other) 
        : size_(other.size_), data_(new char[size_]) {
        std::copy(other.data_, other.data_ + size_, data_);
    }
    
    // Move constructor (cheap!)
    Buffer(Buffer&& other) noexcept
        : size_(other.size_), data_(other.data_) {
        other.size_ = 0;    // Leave source in valid state
        other.data_ = nullptr;
    }
    
    ~Buffer() { delete[] data_; }
};

2. Using std::move

Buffer create_buffer();  // Returns by value

Buffer a = create_buffer();  // Move (or elision)
Buffer b = a;                // Copy! (a is an lvalue)
Buffer c = std::move(a);     // Move! (cast to rvalue)

// After move, 'a' is in a valid but unspecified state
// Don't use 'a' except to assign to or destroy

Perfect Forwarding

Preserving value category when passing arguments through wrapper functions.

// Problem: Without forwarding, we lose value category
template<typename T>
void wrapper_bad(T arg) {  // Always copies!
    process(arg);
}

// Solution: Universal references + std::forward
template<typename T>
void wrapper_good(T&& arg) {  // Universal reference
    process(std::forward<T>(arg));  // Preserves lvalue/rvalue
}

// Usage
int x = 42;
wrapper_good(x);              // T is int&, forwards as lvalue
wrapper_good(42);             // T is int, forwards as rvalue
wrapper_good(std::move(x));   // T is int, forwards as rvalue

Universal vs Rvalue References

ContextT&& means...Example
Template parameterUniversal referencetemplate<typename T> void f(T&&)
Concrete typeRvalue referencevoid f(std::string&&)
Auto&&Universal referenceauto&& x = expr;

Rule of 0/3/5

  • Rule of 0:

    If you can define your class without any custom destructors, copy/move operations, prefer using member types that manage their own resources (smart pointers, containers).

  • Rule of 3:

    If you define a destructor, copy constructor, or copy assignment, you probably need all three.

  • Rule of 5:

    In C++11+, if you define any of destructor/copy/move operations, consider defining all five, or explicitly defaulting/deleting them.

Common Pitfalls

1. Returning std::move

// BAD: Prevents RVO/NRVO
std::vector<int> create() {
    std::vector<int> result;
    // ... fill ...
    return std::move(result);  // Don't do this!
}

// GOOD: Let the compiler optimize
std::vector<int> create() {
    std::vector<int> result;
    // ... fill ...
    return result;  // RVO or move - both good
}

2. Moving const Objects

const std::vector<int> v = {1, 2, 3};
auto v2 = std::move(v);  // Silently copies! (const can't be moved from)

// Always remove const before moving:
auto v2 = std::move(const_cast<std::vector<int>&>(v));  // Dangerous!
// Or better: don't mark it const if you plan to move

Best Practices

  • Mark move constructors and move assignment operators noexcept
  • Use = default for trivial move operations
  • Use = delete to disable move when copy is disabled
  • Follow Rule of 0 when possible (use smart pointers/containers)
  • Don't use std::move on return values (prevents RVO)
  • Don't move from const objects