Move Semantics and Rvalue References: Efficient Resource Management

The Problem: Expensive Copies

Imagine you're moving houses. The old way (copy semantics) is like buying duplicates of all your furniture for the new house, then throwing away the originals. Move semantics is like taking your actual furniture to the new house - much more efficient!

The Old Way: Copy Everything

class String {
private:
    char* data;
    size_t length;
    
public:
    // Copy constructor - expensive for large strings!
    String(const String& other) 
        : length(other.length) {
        data = new char[length + 1];  // Allocate new memory
        std::strcpy(data, other.data); // Copy all characters
    }
    
    // Copy assignment - also expensive!
    String& operator=(const String& other) {
        if (this != &other) {
            delete[] data;  // Clean up old data
            length = other.length;
            data = new char[length + 1];  // Allocate new memory
            std::strcpy(data, other.data); // Copy all characters
        }
        return *this;
    }
};

Understanding Lvalues and Rvalues

Before diving into move semantics, we need to understand lvalues and rvalues. Think of lvalues as things with addresses (like houses) and rvalues as temporary things (like the pizza delivery guy).

graph TD A[Expression] --> B[Lvalue] A --> C[Rvalue] B --> D[Has persistent address] B --> E[Can appear on left of =] B --> F[Examples: variables, array elements] C --> G[Temporary value] C --> H[Cannot take address] C --> I[Examples: literals, temporaries]
int x = 42;        // x is lvalue, 42 is rvalue
int y = x;         // y is lvalue, x is lvalue (lvalue in rvalue context)

int& lr = x;       // OK: lvalue reference to lvalue
int& lr2 = 42;     // ERROR: cannot bind lvalue reference to rvalue

const int& cr = 42; // OK: const lvalue reference can bind to rvalue
int&& rr = 42;     // OK: rvalue reference to rvalue
int&& rr2 = x;     // ERROR: cannot bind rvalue reference to lvalue

// Function returning by value creates an rvalue
std::string getName() { return "temporary"; }
std::string s = getName();  // getName() returns an rvalue

// But you can't do:
// getName() = "something";  // ERROR: can't assign to rvalue

Rvalue References: The && Syntax

Rvalue references (&&) are like temporary ownership papers. They let you take ownership of resources that are about to be destroyed anyway.

Move Constructor and Move Assignment

Now we can implement efficient move operations that transfer ownership instead of copying data.

class String {
private:
    char* data;
    size_t length;
    
public:
    // Move constructor - takes ownership of temporary
    String(String&& other) noexcept
        : data(other.data), length(other.length) {
        // "Steal" the resources
        other.data = nullptr;  // Leave other in valid but empty state
        other.length = 0;
    }
    
    // Move assignment operator
    String& operator=(String&& other) noexcept {
        if (this != &other) {
            delete[] data;  // Clean up current resources
            
            // "Steal" the resources
            data = other.data;
            length = other.length;
            
            // Leave other in valid but empty state
            other.data = nullptr;
            other.length = 0;
        }
        return *this;
    }
    
    // Regular constructors and destructor
    String(const char* str) {
        length = std::strlen(str);
        data = new char[length + 1];
        std::strcpy(data, str);
    }
    
    ~String() {
        delete[] data;
    }
};

The Five/Six Rules

graph TD A[Class Managing Resources] --> B[Rule of Three
C++98] A --> C[Rule of Five
C++11] B --> D[Destructor] B --> E[Copy Constructor] B --> F[Copy Assignment] C --> D C --> E C --> F C --> G[Move Constructor] C --> H[Move Assignment]

std::move: Casting to Rvalue

std::move doesn't actually move anything! It's just a cast that tells the compiler "treat this lvalue as an rvalue". It's like putting a "free to take" sign on your furniture.

std::vector<int> v1 = {1, 2, 3, 4, 5};
std::vector<int> v2 = v1;  // Copy: v1 still has data

std::vector<int> v3 = {1, 2, 3, 4, 5};
std::vector<int> v4 = std::move(v3);  // Move: v3 is now empty

// After move, v3 is in valid but unspecified state
// You can still use v3, but don't assume it has any particular value
v3.clear();  // OK
v3.push_back(10);  // OK

// Common pattern: moving in function returns
std::vector<int> createVector() {
    std::vector<int> local = {1, 2, 3, 4, 5};
    return local;  // No std::move needed! Compiler does RVO/NRVO
}

// When to use std::move
class Widget {
    std::vector<int> data;
public:
    void setData(std::vector<int> newData) {
        data = std::move(newData);  // Move parameter into member
    }
};

Perfect Forwarding with Universal References

Universal references (also called forwarding references) can bind to both lvalues and rvalues, preserving their value category. It's like a postal service that handles both regular mail and express packages appropriately.

// Universal reference: T&& where T is deduced
template<typename T>
void process(T&& param) {  // Universal reference
    // param can be lvalue or rvalue reference
}

// Perfect forwarding: preserve value category
template<typename T>
void wrapper(T&& arg) {
    // std::forward preserves whether arg was lvalue or rvalue
    actualFunction(std::forward<T>(arg));
}

// Practical example: make_unique before C++14
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

// Usage
auto p1 = make_unique<String>("Hello");  // Forward rvalue
String s = "World";
auto p2 = make_unique<String>(s);  // Forward lvalue (copies)
auto p3 = make_unique<String>(std::move(s));  // Forward rvalue (moves)
graph LR A[wrapper called with] --> B[Lvalue] A --> C[Rvalue] B --> D[forward preserves lvalue] C --> E[forward preserves rvalue] D --> F[actualFunction gets lvalue] E --> G[actualFunction gets rvalue]

Move Semantics in Standard Containers

All standard containers support move semantics, making operations like returning containers from functions extremely efficient.

// Efficient container operations
std::vector<std::string> readFile(const std::string& filename) {
    std::vector<std::string> lines;
    // ... read file into lines ...
    return lines;  // Move, not copy! (NRVO or move)
}

// Container of containers
std::vector<std::vector<int>> matrix;
std::vector<int> row = {1, 2, 3, 4, 5};

// Old way (copy)
matrix.push_back(row);  // row is still valid

// New way (move)
matrix.push_back(std::move(row));  // row is now empty

// Moving elements within containers
std::vector<std::string> v1 = {"apple", "banana", "cherry"};
std::vector<std::string> v2;

// Move all elements
v2 = std::move(v1);  // v1 is now empty

// Move individual elements
std::vector<std::unique_ptr<Widget>> widgets;
widgets.push_back(std::make_unique<Widget>());  // Move-only type

// Algorithms with move
std::vector<std::string> source = {"a", "b", "c"};
std::vector<std::string> dest;
std::move(source.begin(), source.end(), std::back_inserter(dest));

Common Patterns and Best Practices

The Copy-and-Swap Idiom

class String {
    // ... other members ...
    
    // Unified assignment operator handles both copy and move
    String& operator=(String other) {  // Pass by value
        swap(other);
        return *this;
    }  // other's destructor cleans up our old data
    
    void swap(String& other) noexcept {
        std::swap(data, other.data);
        std::swap(length, other.length);
    }
};

Move-Only Types

class FileHandle {
    FILE* file;
    
public:
    // Delete copy operations
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;
    
    // Default move operations
    FileHandle(FileHandle&&) = default;
    FileHandle& operator=(FileHandle&&) = default;
    
    // Now FileHandle can only be moved, not copied
    // Perfect for unique resources like file handles
};

When to Use std::move

Practical Examples

Exercise: Implement a Move-Enabled Buffer Class

template<typename T>
class Buffer {
private:
    T* data;
    size_t size;
    
public:
    // Constructor
    explicit Buffer(size_t n) 
        : data(new T[n]), size(n) {}
    
    // Destructor
    ~Buffer() { delete[] data; }
    
    // TODO: Implement
    // 1. Copy constructor
    // 2. Copy assignment
    // 3. Move constructor
    // 4. Move assignment
    
    // Hints:
    // - Copy should allocate new memory and copy elements
    // - Move should transfer ownership and set source to nullptr
    // - Remember to handle self-assignment
    // - Mark move operations as noexcept
};

Exercise: Perfect Forwarding Factory

// Create a factory function that perfectly forwards arguments
template<typename T, typename... Args>
T* factory(Args&&... args) {
    std::cout << "Creating object with " 
              << sizeof...(args) << " arguments\n";
    
    // TODO: Use perfect forwarding to construct T
    // Hint: Use std::forward<Args>(args)...
}

// Test with:
struct Widget {
    Widget(int x) { std::cout << "Widget(int)\n"; }
    Widget(const std::string& s) { std::cout << "Widget(string&)\n"; }
    Widget(std::string&& s) { std::cout << "Widget(string&&)\n"; }
};

Exercise: Move-Only Message Queue

// Implement a thread-safe queue for move-only types
template<typename T>
class MoveOnlyQueue {
private:
    std::queue<T> queue;
    mutable std::mutex mutex;
    
public:
    // Only allow moving in
    void push(T&& value) {
        // TODO: Lock mutex and move value into queue
    }
    
    // Only allow moving out
    std::optional<T> pop() {
        // TODO: Lock mutex, check if empty
        // If not empty, move front element out
        // Return std::nullopt if empty
    }
};

Performance Impact

Move semantics can dramatically improve performance, especially for containers and resource-heavy objects.

// Benchmark example
#include <chrono>
#include <vector>

void benchmarkCopyVsMove() {
    const size_t SIZE = 1000000;
    std::vector<int> source(SIZE, 42);
    
    // Benchmark copy
    auto start = std::chrono::high_resolution_clock::now();
    std::vector<int> copied = source;  // Copy
    auto copy_time = std::chrono::high_resolution_clock::now() - start;
    
    // Benchmark move
    start = std::chrono::high_resolution_clock::now();
    std::vector<int> moved = std::move(source);  // Move
    auto move_time = std::chrono::high_resolution_clock::now() - start;
    
    std::cout << "Copy time: " 
              << std::chrono::duration_cast<std::chrono::microseconds>(copy_time).count() 
              << " μs\n";
    std::cout << "Move time: " 
              << std::chrono::duration_cast<std::chrono::microseconds>(move_time).count() 
              << " μs\n";
    
    // Typical result:
    // Copy time: 2000 μs
    // Move time: 1 μs
}

Common Pitfalls and Solutions

Pitfall: Moving from const objects

const std::vector<int> v = {1, 2, 3};
std::vector<int> v2 = std::move(v);  // Doesn't move! Copies instead!
// std::move on const object returns const T&&, which matches copy constructor

Pitfall: Use after move

std::string s = "Hello";
std::string s2 = std::move(s);
std::cout << s;  // Undefined behavior? No! But s is empty
// Standard library types guarantee valid but unspecified state

Pitfall: Returning std::move

// BAD - prevents RVO
std::vector<int> bad() {
    std::vector<int> v = {1, 2, 3};
    return std::move(v);  // Don't do this!
}

// GOOD - allows RVO/NRVO
std::vector<int> good() {
    std::vector<int> v = {1, 2, 3};
    return v;  // Compiler optimizes
}

Next Steps

You've mastered the essentials of move semantics and rvalue references:

Move semantics transformed C++ from a language that copied everything to one that can efficiently transfer resources. It's like upgrading from photocopying documents to simply handing them over. In our next lesson, we'll explore Design Patterns in Modern C++, showing how to structure your code for maximum flexibility and maintainability.