Module 7.3

Rvalue References and Move Semantics

Unlock the power of modern C++ with rvalue references and move semantics. Learn how to write blazing-fast code by avoiding unnecessary copies and transferring resources efficiently!

40 min read
Advanced
Hands-on Examples
What You'll Learn
  • Lvalues vs Rvalues explained
  • Rvalue references (&&) syntax
  • Move constructors and assignment
  • std::move and std::forward
  • Perfect forwarding in templates
Contents
01

Lvalues vs Rvalues

Before diving into rvalue references, you need to understand the fundamental distinction between lvalues and rvalues. This concept is the foundation of move semantics.

What are Lvalues and Rvalues?

Every expression in C++ has a value category. The two most important categories are:

Lvalue (Left Value)

Has identity - refers to a memory location

  • Has a name you can refer to
  • You can take its address with &
  • Persists beyond the expression
  • Can appear on the left side of assignment
Rvalue (Right Value)

No identity - temporary, about to expire

  • No name (usually)
  • Cannot take its address
  • Destroyed at end of expression
  • Can only appear on the right side
#include <iostream>
#include <string>

int main() {
    // LVALUES - have identity, persist
    int x = 10;           // x is an lvalue
    int* ptr = &x;        // Can take address of lvalue
    int& ref = x;         // Can bind lvalue reference to lvalue
    
    int arr[5] = {1, 2, 3, 4, 5};
    arr[0] = 100;         // arr[0] is an lvalue
    
    std::string name = "Alice";
    name[0] = 'E';        // name[0] is an lvalue

This code demonstrates lvalues - expressions with identity that persist beyond their use. Variables like x and name are lvalues because they have memory addresses you can take with &. Array elements like arr[0] and string characters like name[0] are also lvalues because they refer to specific memory locations. You can modify them, take their address, and create references to them.

    // RVALUES - temporary, no identity
    // 42;                // Literal 42 is an rvalue
    // x + 5;             // Expression result is an rvalue
    // getName();         // Return value (if not reference) is an rvalue
    
    int y = 42;           // 42 is rvalue, assigned to lvalue y
    int z = x + 5;        // (x + 5) is rvalue, assigned to lvalue z

Rvalues are temporary values without identity. Literals like 42 and expression results like x + 5 are rvalues - they exist only momentarily during computation and then disappear. You can use them on the right side of assignments (hence "rvalue"), but they don't have memory addresses you can capture. Function return values (when not returning by reference) are also rvalues because they're temporary objects about to be destroyed.

    // These would NOT compile:
    // 42 = x;            // Error: cannot assign to rvalue
    // int* p = &42;      // Error: cannot take address of rvalue
    // int& r = 42;       // Error: cannot bind lvalue ref to rvalue
    
    return 0;
}

These lines (commented out) would cause compilation errors because they violate fundamental C++ rules. You cannot assign to an rvalue like 42 because it's not a memory location - where would the value go? You can't take the address of 42 because it doesn't exist in memory - it's just a constant value. And you can't bind a regular lvalue reference to 42 because references must point to objects that persist, not temporaries that immediately disappear.

Simple Mental Model

Quick Rule: If you can take its address (&), it's an lvalue. If you can't, it's an rvalue. Think: "Can I point to it?"
Expression Category Why?
x (variable) lvalue Has a name, can take &x
arr[i] lvalue Refers to specific memory, can take &arr[i]
*ptr lvalue Dereference gives identity, can take &(*ptr)
++x (pre-increment) lvalue Returns reference to x itself
42 (literal) rvalue No memory location, cannot take &42
x + y rvalue Temporary result, cannot take &(x+y)
x++ (post-increment) rvalue Returns copy of old value, not x itself
std::string("hello") rvalue Temporary object, about to be destroyed

Why Does This Matter?

The distinction between lvalues and rvalues becomes crucial when we want to optimize our code. Consider this scenario:

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

std::vector<int> createLargeVector() {
    std::vector<int> v(1000000);  // 1 million integers
    // ... fill vector with data ...
    return v;  // Returns a temporary (rvalue)
}

int main() {
    // Old C++ (pre-C++11) would COPY the entire vector
    // That's 4MB of unnecessary copying!
    std::vector<int> result = createLargeVector();
    
    // With move semantics, we can DETECT that createLargeVector()
    // returns a temporary (rvalue) and STEAL its resources
    // instead of copying. The temporary is about to die anyway!
    
    return 0;
}
Without Move Semantics

Temporary vector created, then copied (4MB), then destroyed. Wasteful!

With Move Semantics

Temporary's internal pointer is moved (just a pointer copy). Near instant!

Practice Questions: Lvalues vs Rvalues

Given:

int arr[] = {10, 20, 30, 40, 50};

Task: Write a function getElement(int* arr, int index) that returns a reference to the element at the given index, allowing modification through the return value.

Expected output when calling getElement(arr, 2) = 100;: Array becomes {10, 20, 100, 40, 50}

Show Solution
#include <iostream>

int& getElement(int* arr, int index) {
    return arr[index];  // Returns lvalue reference
}

int main() {
    int arr[] = {10, 20, 30, 40, 50};
    
    getElement(arr, 2) = 100;  // Modify through reference
    
    for (int x : arr) std::cout << x << " ";
    // Output: 10 20 100 40 50
}

Given:

std::string createMessage(const std::string& name) {
    return "Hello, " + name + "!";
}

Task: Call createMessage and store the result in a const lvalue reference. Then print the message length and content to prove the temporary's lifetime was extended.

Expected output for name "Alice": Length: 13, Message: Hello, Alice!

Show Solution
#include <iostream>
#include <string>

std::string createMessage(const std::string& name) {
    return "Hello, " + name + "!";
}

int main() {
    // Const reference extends temporary lifetime
    const std::string& msg = createMessage("Alice");
    
    // Temporary is still valid here!
    std::cout << "Length: " << msg.length() 
              << ", Message: " << msg << std::endl;
    // Output: Length: 13, Message: Hello, Alice!
}

Given:

int x = 10;
const int cx = 20;

Task: Write three overloaded functions named identify that detect and print whether the argument is: (1) non-const lvalue, (2) const lvalue, or (3) rvalue. Test with x, cx, and x + cx.

Expected output: non-const lvalue, const lvalue, rvalue

Show Solution
#include <iostream>

void identify(int&) { 
    std::cout << "non-const lvalue" << std::endl; 
}
void identify(const int&) { 
    std::cout << "const lvalue" << std::endl; 
}
void identify(int&&) { 
    std::cout << "rvalue" << std::endl; 
}

int main() {
    int x = 10;
    const int cx = 20;
    
    identify(x);       // non-const lvalue
    identify(cx);      // const lvalue
    identify(x + cx);  // rvalue
}
02

Rvalue References (&&)

Rvalue references, declared with &&, are a new type of reference introduced in C++11 that can bind to temporary objects. They are the key to enabling move semantics.

The && Syntax

C++11 introduced a brand new reference type using the && syntax (two ampersands). If you're familiar with regular references (&, one ampersand), which are technically called "lvalue references," then rvalue references (&&) are their counterpart.

Here's the key difference: while lvalue references can only bind to lvalues (named objects with identity), rvalue references can only bind to rvalues (temporary objects about to be destroyed). This new ability to "grab hold" of temporaries is what enables move semantics!

Think of it this way: regular references (&) let you create an alias to an existing object. Rvalue references (&&) let you grab onto temporary objects that are about to disappear, giving you a chance to salvage their resources before they're destroyed.

Reference Type Syntax Binds To Primary Use
Lvalue Reference T& Lvalues only Aliasing existing objects
Const Lvalue Reference const T& Both lvalues and rvalues Read-only access, accepting any value
Rvalue Reference T&& Rvalues only Move semantics, detecting temporaries
#include <iostream>
#include <string>

int main() {
    int x = 10;
    
    // Lvalue reference - binds to lvalue
    int& lref = x;          // OK: x is an lvalue
    // int& lref2 = 42;     // ERROR: 42 is an rvalue

Regular lvalue references (&) can only bind to lvalues - named objects with identity. Here, lref successfully binds to x because x is a variable with a memory address. However, trying to bind to 42 fails because literals are rvalues with no identity. This is the traditional C++ reference behavior you're familiar with.

    // Const lvalue reference - binds to both
    const int& clref1 = x;  // OK: lvalue
    const int& clref2 = 42; // OK: rvalue (special rule)

Const lvalue references (const T&) have a special power - they can bind to both lvalues AND rvalues! This is an exception in C++ that makes const references very flexible. When you bind a const reference to an rvalue like 42, the compiler creates a hidden temporary variable to hold that value, and the reference points to it. This is why const references are commonly used for function parameters.

    // Rvalue reference - binds to rvalue
    int&& rref = 42;        // OK: 42 is an rvalue
    // int&& rref2 = x;     // ERROR: x is an lvalue

Rvalue references (&&) are the opposite of lvalue references - they can ONLY bind to rvalues (temporaries). Here, rref successfully binds to the literal 42, but attempting to bind to x fails because x is an lvalue. This selectivity is intentional - it lets the compiler distinguish between "I can safely steal from this" (rvalue) versus "I must be careful with this" (lvalue).

    // We can modify through rvalue reference!
    rref = 100;
    std::cout << rref << std::endl;  // Output: 100
    
    // String example
    std::string&& strRef = std::string("temporary");
    std::cout << strRef << std::endl;  // Output: temporary
    
    return 0;
}

Unlike const references, rvalue references allow modification! Once we've bound rref to the temporary, we can modify it just like a regular variable. This demonstrates that rvalue references give you full access to temporaries before they're destroyed. The string example shows the same concept - we bind to a temporary string object and can use it normally. These temporaries live until the reference goes out of scope.

Important! Once bound to a name, an rvalue reference becomes an lvalue. The variable rref above is an lvalue (it has a name and address), even though it's declared as int&&.

Extending Temporary Lifetime

One of the most useful features of both const T& and T&& is their ability to extend the lifetime of temporary objects. This is a special rule in C++ that might seem like magic at first!

Normally, a temporary object (like the return value of a function) is destroyed at the end of the statement where it was created. But when you bind that temporary to a reference, C++ says: "Okay, I'll keep this object alive as long as the reference exists." This prevents dangling references and makes your code safer.

Why is this useful? It lets you work with temporary objects as if they were regular variables, extending their usefulness beyond a single statement. You can inspect them, modify them (with T&&), and even pass them to other functions - all while the temporary is guaranteed to stay alive.

#include <iostream>
#include <string>

std::string createGreeting() {
    return "Hello, World!";  // Returns temporary
}

This simple function returns a string by value, which means it returns a temporary object. Normally, this temporary would be destroyed immediately after the function call completes. But we can use rvalue references to capture and extend the lifetime of this temporary, allowing us to work with it beyond a single statement.

int main() {
    // Without binding - temporary destroyed immediately
    // createGreeting();  // Temporary exists only for this line
    
    // Bind to rvalue reference - lifetime extended!
    std::string&& greeting = createGreeting();

By binding the returned temporary to an rvalue reference, we trigger C++'s lifetime extension rule. The temporary string that createGreeting() returns would normally be destroyed at the semicolon, but because we bind it to greeting, C++ keeps it alive for the entire scope. This is safe and efficient - no copying happens, just a reference to the temporary.

    // greeting is valid for the rest of this scope
    std::cout << greeting << std::endl;  // Safe to use
    
    // Can even modify it
    greeting += " How are you?";
    std::cout << greeting << std::endl;  // "Hello, World! How are you?"
    
    return 0;
}  // greeting destroyed here

The extended temporary behaves just like a regular variable - we can read it, modify it, and pass it around. The modification greeting += " How are you?"; works because rvalue references aren't const. This temporary string stays alive until the reference goes out of scope at the closing brace. This pattern is incredibly useful for capturing function return values efficiently.

Function Overloading with References

One of the most powerful techniques with rvalue references is function overloading. You can write multiple versions of the same function - one that handles lvalues and another that handles rvalues. The compiler will automatically choose the right version based on what you pass in!

Why would you do this? Because you can optimize each version differently. The lvalue version might need to copy data (since the original is still being used), while the rvalue version can steal resources (since the temporary is about to be destroyed anyway). This is the foundation of move semantics!

Think of it like having two doors: a "copy entrance" for valuable items you still need, and an "express entrance" for things you're throwing away. The compiler automatically routes your objects to the right door.

#include <iostream>
#include <string>

// Overload for lvalues - receives existing objects
void process(const std::string& str) {
    std::cout << "Lvalue version: " << str << std::endl;
}

// Overload for rvalues - receives temporaries
void process(std::string&& str) {
    std::cout << "Rvalue version: " << str << std::endl;
    // We can "steal" from str here since it's temporary!
}

Here we define two versions of the process function - one accepting const std::string& (for lvalues) and one accepting std::string&& (for rvalues). The compiler uses overload resolution to automatically select the right version based on the argument's value category. The lvalue version takes a const reference because we shouldn't modify objects that the caller might still need. The rvalue version can be optimized differently since it receives temporaries.

int main() {
    std::string name = "Alice";
    
    process(name);              // Calls lvalue version
    process("Bob");             // Calls rvalue version (literal -> temporary)
    process(std::string("Eve"));  // Calls rvalue version (explicit temporary)
    process(name + "!");        // Calls rvalue version (concatenation = temporary)
    
    return 0;
}
// Output:
// Lvalue version: Alice
// Rvalue version: Bob
// Rvalue version: Eve
// Rvalue version: Alice!

The compiler's overload resolution automatically routes each call to the appropriate version. When we pass name, an lvalue, the const reference version is called. When we pass string literals, explicit temporaries like std::string("Eve"), or expression results like name + "!", the rvalue reference version is called. This automatic dispatch is the key to efficient move semantics - the compiler knows when it's safe to optimize (rvalues) versus when it must be careful (lvalues).

Reference Binding Rules
LVALUE
has name / identity
RVALUE
temporary / expiring
T&

Lvalue Reference

Lvalue Rvalue
const T&

Const Lvalue Reference

Lvalue Rvalue
T&&

Rvalue Reference

Lvalue Rvalue
Key Insight: const T& is the "universal receiver" - it accepts both lvalues and rvalues!

The "Named Rvalue Reference" Gotcha

Here's one of the most confusing aspects of C++ move semantics, and it trips up even experienced programmers: a named rvalue reference is itself an lvalue!

Let that sink in. Even though you declare a variable as int&& x, once x has a name, it becomes an lvalue. Why? Because the fundamental rule still applies: if something has a name and persists across multiple statements, it's an lvalue. The && only describes what kind of value x can bind to initially, not what x itself is.

This means if you receive an rvalue reference parameter in a function and try to pass it to another function, it won't bind to that function's rvalue reference parameter - you'll accidentally call the lvalue version instead! The solution is to use std::move() to cast it back to an rvalue.

#include <iostream>
#include <string>

void takeRvalue(std::string&& str) {
    std::cout << "Got rvalue: " << str << std::endl;
}

void forward(std::string&& str) {
    // str is a NAMED variable, so it's an lvalue!
    // takeRvalue(str);        // ERROR: str is lvalue
    
    // Must cast back to rvalue to forward
    takeRvalue(std::move(str));  // OK: std::move casts to rvalue
}

int main() {
    forward(std::string("Hello"));
    return 0;
}
Remember: If it has a name, it's an lvalue - regardless of how it was declared. This is why we need std::move()!

Practice Questions: Rvalue References

Given:

std::string concatenate(const std::string& a, const std::string& b) {
    return a + " " + b;
}

Task: Use an rvalue reference to capture the temporary returned by concatenate("Hello", "World"), then modify the captured string by appending "!" and print the result.

Expected output: Hello World!

Show Solution
#include <iostream>
#include <string>

std::string concatenate(const std::string& a, const std::string& b) {
    return a + " " + b;
}

int main() {
    // Rvalue reference extends temporary lifetime
    std::string&& result = concatenate("Hello", "World");
    
    // Can modify through rvalue reference!
    result += "!";
    
    std::cout << result << std::endl;  // Hello World!
}

Given:

std::vector<int> data = {1, 2, 3};
std::vector<int> createVector() { return {4, 5, 6}; }

Task: Write two overloaded functions named process: one taking std::vector<int>& that prints "Processing lvalue", and one taking std::vector<int>&&& that prints "Processing rvalue". Test with both data and createVector().

Expected output: Processing lvalue then Processing rvalue

Show Solution
#include <iostream>
#include <vector>

void process(std::vector<int>&) {
    std::cout << "Processing lvalue" << std::endl;
}

void process(std::vector<int>&&&) {
    std::cout << "Processing rvalue" << std::endl;
}

std::vector<int> createVector() { return {4, 5, 6}; }

int main() {
    std::vector<int> data = {1, 2, 3};
    
    process(data);           // Processing lvalue
    process(createVector()); // Processing rvalue
}

Given:

void consume(std::string& s) { std::cout << "lvalue: " << s; }
void consume(std::string&& s) { std::cout << "rvalue: " << s; }

Task: Write a function relay(std::string&& msg) that receives an rvalue reference and forwards it to consume as an rvalue (not as an lvalue). Then call relay with a temporary string "Hello".

Expected output: rvalue: Hello

Hint: Named rvalue references are lvalues. Use std::move() to cast back.

Show Solution
#include <iostream>
#include <string>
#include <utility>

void consume(std::string& s) { std::cout << "lvalue: " << s; }
void consume(std::string&& s) { std::cout << "rvalue: " << s; }

void relay(std::string&& msg) {
    // msg is named, so it's an lvalue!
    // Must use std::move to forward as rvalue
    consume(std::move(msg));
}

int main() {
    relay(std::string("Hello"));  // rvalue: Hello
}
03

Move Semantics

Move semantics allow you to transfer resources from one object to another instead of copying. This dramatically improves performance when working with objects that manage resources like memory.

The Problem with Copying

Before C++11, when you passed objects around or returned them from functions, C++ would copy the entire object. For simple types like integers, this is fine. But for objects that manage resources (like memory, file handles, or network connections), copying can be very expensive!

Consider a class that manages a dynamic array. Every time we copy it, we must:

  1. Allocate new memory (expensive!)
  2. Copy every single element from the old array to the new one (expensive!)
  3. Keep both the original and the copy alive

But here's the key insight: if we're copying from a temporary object that's about to be destroyed anyway, why copy at all? We could just "steal" its resources instead! This is exactly what move semantics does.

#include <iostream>
#include <algorithm>

class Buffer {
public:
    Buffer(size_t size) : size_(size), data_(new int[size]) {
        std::cout << "Constructor: allocated " << size << " ints" << std::endl;
    }
    
    // Copy constructor - EXPENSIVE!
    Buffer(const Buffer& other) : size_(other.size_), data_(new int[other.size_]) {
        std::copy(other.data_, other.data_ + size_, data_);
        std::cout << "Copy constructor: copied " << size_ << " ints" << std::endl;
    }
    
    ~Buffer() {
        delete[] data_;
        std::cout << "Destructor: freed memory" << std::endl;
    }
    
private:
    size_t size_;
    int* data_;
};

Buffer createBuffer() {
    Buffer temp(1000000);  // 1 million ints
    return temp;
}

int main() {
    Buffer b = createBuffer();  // Copy constructor called (without move semantics)
    return 0;
}
Without move semantics: 1 million integers allocated, then copied, then the original is destroyed. That's 4MB copied unnecessarily!

Move Constructor and Move Assignment

A move constructor "steals" resources from a temporary instead of copying. It takes an rvalue reference parameter and transfers ownership.

#include <iostream>
#include <algorithm>
#include <utility>

class Buffer {
public:
    Buffer(size_t size) : size_(size), data_(new int[size]) {
        std::cout << "Constructor: allocated " << size << " ints" << std::endl;
    }
    
private:
    size_t size_;
    int* data_;
};

The Buffer class manages dynamically allocated memory for an array of integers. The constructor takes a size parameter, allocates memory on the heap using new int[size], and stores both the size and pointer in member variables. This is a typical resource-owning class that will benefit from move semantics. Notice we're printing allocation messages to help us see when resources are allocated, copied, or moved.

// Copy constructor - for lvalues
Buffer(const Buffer& other) : size_(other.size_), data_(new int[other.size_]) {
    std::copy(other.data_, other.data_ + size_, data_);
    std::cout << "Copy constructor: copied " << size_ << " ints" << std::endl;
}

When you copy a Buffer object, the copy constructor must allocate new memory and copy every single integer from the source buffer to the destination. For a buffer with 1 million integers, that means allocating 4MB of memory and copying 4MB of data - a very slow operation! This is necessary when copying lvalues because the original object must remain unchanged. The const Buffer& parameter ensures we can't modify the source during copying.

// MOVE constructor - for rvalues (temporaries)
Buffer(Buffer&& other) noexcept 
    : size_(other.size_), data_(other.data_) {
    // Steal the resources
    other.size_ = 0;
    other.data_ = nullptr;  // Leave source in valid state
    std::cout << "Move constructor: MOVED " << size_ << " ints (no copy!)" << std::endl;
}

The magic happens here! Instead of allocating new memory and copying data, the move constructor simply "steals" the pointer from the source object. We take ownership of other.data_ by copying the pointer value (just 8 bytes on 64-bit systems), then set the source's pointer to nullptr. This prevents the source from deleting our data when it's destroyed. The entire operation is just a few pointer assignments - incredibly fast regardless of buffer size! The noexcept is crucial for STL container optimizations.

// Copy assignment operator
Buffer& operator=(const Buffer& other) {
    if (this != &other) {
        delete[] data_;
        size_ = other.size_;
        data_ = new int[size_];
        std::copy(other.data_, other.data_ + size_, data_);
        std::cout << "Copy assignment: copied " << size_ << " ints" << std::endl;
    }
    return *this;
}

Copy assignment is called when you assign one existing Buffer to another using = with an lvalue on the right side (e.g., b1 = b2;). First, we check for self-assignment to avoid accidentally deleting our own data. Then we free the existing memory, allocate new memory to match the source size, and copy all the data over. Like the copy constructor, this is expensive for large buffers because every element must be copied.

// MOVE assignment operator
Buffer& operator=(Buffer&& other) noexcept {
    if (this != &other) {
        delete[] data_;  // Free existing resource
        // Steal from other
        size_ = other.size_;
        data_ = other.data_;
        // Leave other in valid state
        other.size_ = 0;
        other.data_ = nullptr;
        std::cout << "Move assignment: MOVED " << size_ << " ints (no copy!)" << std::endl;
    }
    return *this;
}

Move assignment is called when assigning a temporary (rvalue) to an existing object. We first free our current resources (since we're replacing them), then steal the pointer from the source just like in the move constructor. After stealing, we set the source's members to safe default values. This is vastly more efficient than copy assignment - we're just swapping a few pointers instead of copying potentially millions of integers. The noexcept guarantee is important for exception safety.

~Buffer() {
    delete[] data_;
    if (size_ > 0) {
        std::cout << "Destructor: freed " << size_ << " ints" << std::endl;
    }
}

The destructor is responsible for freeing the dynamically allocated memory to prevent memory leaks. It calls delete[] data_; which is safe even if data_ is nullptr (which happens after an object has been moved from). We check if size_ > 0 before printing the message to avoid printing "freed 0 ints" for moved-from objects, keeping the output clean.

Buffer createBuffer() {
    return Buffer(1000000);  // Temporary - move constructor will be used
}

int main() {
    Buffer b = createBuffer();  // Move constructor called!
    
    Buffer b2(100);
    b2 = createBuffer();        // Move assignment called!
    
    return 0;
}

Now let's see move semantics in action! The createBuffer() function returns a temporary Buffer object. When we assign it to b, the compiler recognizes it's a temporary (rvalue) and calls the move constructor instead of the copy constructor - saving us from copying 1 million integers! Similarly, when we assign the return value of createBuffer() to the existing b2, the move assignment operator is called. Watch the console output to see the difference!

Copy (Expensive)
  1. Allocate new memory
  2. Copy every element
  3. Now have two independent copies
  4. Time: O(n)
Move (Cheap)
  1. Copy the pointer (just a few bytes)
  2. Set source pointer to null
  3. Resources transferred, source is "empty"
  4. Time: O(1)

std::move - Enabling Move on Lvalues

Move constructors are called automatically for rvalues. But what if you have an lvalue that you want to move from? Use std::move() to cast it to an rvalue.

std::move() doesn't move anything! It's just a cast that converts an lvalue to an rvalue reference, enabling the move constructor/assignment to be called.
#include <iostream>
#include <string>
#include <vector>
#include <utility>

int main() {
    std::string str1 = "Hello, World!";
    
    // Copy - str1 still valid
    std::string str2 = str1;
    std::cout << "After copy: str1 = '" << str1 << "'" << std::endl;
    
    // Move - str1 is now in "moved-from" state
    std::string str3 = std::move(str1);
    std::cout << "After move: str1 = '" << str1 << "'" << std::endl;  // Likely empty
    std::cout << "str3 = '" << str3 << "'" << std::endl;
    
    // Vector example
    std::vector<int> v1 = {1, 2, 3, 4, 5};
    std::vector<int> v2 = std::move(v1);
    
    std::cout << "v1 size after move: " << v1.size() << std::endl;  // 0
    std::cout << "v2 size after move: " << v2.size() << std::endl;  // 5
    
    return 0;
}
// Output:
// After copy: str1 = 'Hello, World!'
// After move: str1 = ''
// str3 = 'Hello, World!'
// v1 size after move: 0
// v2 size after move: 5

When to Use std::move

Now that you understand what std::move() does (casts to rvalue), the natural question is: when should you use it? The answer requires careful thought, because using std::move() incorrectly can lead to subtle bugs where your objects are left in unexpected states.

The golden rule is: only use std::move() when you're truly done with the object and you're okay with it being left empty or in a "moved-from" state. After you move from an object, you shouldn't use it again (except to assign it a new value or let it be destroyed).

DO Use std::move
  • When passing to a sink function that will own the data
  • When you're done with an object and want to transfer it
  • When pushing to containers and you don't need the original
  • When implementing move constructor/assignment
DON'T Use std::move
  • On return statements (prevents RVO/NRVO)
  • On const objects (will silently copy instead)
  • When you still need the object afterward
  • On trivially copyable types (int, double, etc.)
#include <iostream>
#include <string>
#include <vector>

void takeOwnership(std::string s) {
    std::cout << "Received: " << s << std::endl;
}

This function takes a std::string by value, which means it will take ownership of the string passed to it. When we pass an rvalue (like a temporary or a moved-from object), the string's move constructor is automatically called - no copy happens! This is a "sink" function that consumes its parameter, making it a perfect candidate for accepting moved-from objects.

std::string createString() {
    std::string result = "Hello";
    return result;  // DON'T use std::move here! RVO will handle it
    // return std::move(result);  // WRONG - prevents optimization
}

This demonstrates a common mistake: using std::move on return statements. Modern compilers perform Return Value Optimization (RVO) or Named Return Value Optimization (NRVO), which eliminates the copy entirely - not even a move! When you add std::move, you actually prevent this optimization and force a move that wouldn't have been necessary. Let the compiler optimize; trust the return value optimization!

int main() {
    std::string name = "Alice";
    
    // Good: we're done with name
    takeOwnership(std::move(name));
    // Don't use name anymore!
    
    // Good: moving into container
    std::vector<std::string> names;
    std::string newName = "Bob";
    names.push_back(std::move(newName));  // Efficient!

Here we see two good uses of std::move. First, we move name into the sink function - we're explicitly saying "I'm done with this variable, take it." Second, we move newName into the vector using push_back. This avoids copying the string and instead transfers its internal buffer directly to the vector's storage. Both cases follow the golden rule: we don't use these variables after moving from them.

    // Bad: const prevents actual move
    const std::string constant = "Immutable";
    std::string copy = std::move(constant);  // Actually copies!
    
    return 0;
}

This demonstrates a subtle trap: moving from const objects. Move semantics requires modifying the source object (setting its internal pointer to null, for example), but const objects can't be modified! So even though we use std::move, the compiler silently falls back to calling the copy constructor instead. The code compiles without error but performs an expensive copy, completely defeating the purpose of using std::move.

The Rule of Five

In C++ programming, there's an important guideline called the Rule of Five. It states: if your class needs to define any one of the following special member functions, you probably need to define all five of them.

Why? If your class manages a resource (like dynamically allocated memory, file handles, or network connections), you need to carefully control what happens when objects are created, copied, moved, assigned, and destroyed. Defining just one or two of these functions usually means your class needs special handling for all of them.

The five special member functions are:

class Resource {
public:
    // 1. Destructor
    ~Resource();
    
    // 2. Copy constructor
    Resource(const Resource& other);
    
    // 3. Copy assignment
    Resource& operator=(const Resource& other);
    
    // 4. Move constructor
    Resource(Resource&& other) noexcept;
    
    // 5. Move assignment
    Resource& operator=(Resource&& other) noexcept;
};
Pro Tip: Mark move operations as noexcept! STL containers like std::vector will only use move operations during reallocation if they're guaranteed not to throw.

Practice Questions: Move Semantics

Given:

std::string original = "This is a very long string that we want to move";

Task: Transfer the string to a new variable moved using std::move. Print both strings and their sizes to demonstrate the move occurred.

Expected output: original should be empty (size 0), moved should contain the string.

Show Solution
#include <iostream>
#include <string>
#include <utility>

int main() {
    std::string original = "This is a very long string that we want to move";
    
    // Move the string
    std::string moved = std::move(original);
    
    std::cout << "original: '" << original << "' (size: " 
              << original.size() << ")" << std::endl;
    std::cout << "moved: '" << moved << "' (size: " 
              << moved.size() << ")" << std::endl;
    
    // Output:
    // original: '' (size: 0)
    // moved: 'This is a very long string...' (size: 47)
}

Given:

class IntBuffer {
    int* data_;
    size_t size_;
public:
    IntBuffer(size_t n) : data_(new int[n]), size_(n) {
        std::cout << "Allocated " << n << " ints" << std::endl;
    }
    ~IntBuffer() { 
        delete[] data_; 
        std::cout << "Freed memory" << std::endl;
    }
    size_t size() const { return size_; }
    // TODO: Add move constructor
};

Task: Add a move constructor that steals the pointer from the source object. Test by creating a buffer and moving it to another variable. Verify only one allocation and one deallocation occurs.

Expected output: "Allocated" once, "Freed" once (not twice).

Show Solution
#include <iostream>
#include <utility>

class IntBuffer {
    int* data_;
    size_t size_;
public:
    IntBuffer(size_t n) : data_(new int[n]), size_(n) {
        std::cout << "Allocated " << n << " ints" << std::endl;
    }
    
    // Move constructor - steal the pointer!
    IntBuffer(IntBuffer&& other) noexcept 
        : data_(other.data_), size_(other.size_) {
        other.data_ = nullptr;  // Prevent double delete
        other.size_ = 0;
        std::cout << "Moved (no allocation)" << std::endl;
    }
    
    ~IntBuffer() { 
        if (data_) {
            delete[] data_; 
            std::cout << "Freed memory" << std::endl;
        }
    }
    size_t size() const { return size_; }
};

int main() {
    IntBuffer buf1(1000);
    IntBuffer buf2 = std::move(buf1);  // Move, not copy!
    
    std::cout << "buf1 size: " << buf1.size() << std::endl;  // 0
    std::cout << "buf2 size: " << buf2.size() << std::endl;  // 1000
}

Given:

class UniqueString {
    char* data_;
    size_t length_;
public:
    UniqueString(const char* str) {
        length_ = strlen(str);
        data_ = new char[length_ + 1];
        strcpy(data_, str);
    }
    const char* c_str() const { return data_; }
    // TODO: Implement Rule of Five
};

Task: Implement all five special member functions: destructor, copy constructor, copy assignment, move constructor, and move assignment operator. Test by creating, copying, and moving UniqueString objects.

Hint: Use copy-and-swap idiom for exception safety.

Show Solution
#include <iostream>
#include <cstring>
#include <utility>

class UniqueString {
    char* data_;
    size_t length_;
public:
    // Constructor
    UniqueString(const char* str) {
        length_ = strlen(str);
        data_ = new char[length_ + 1];
        strcpy(data_, str);
    }
    
    // 1. Destructor
    ~UniqueString() { delete[] data_; }
    
    // 2. Copy constructor
    UniqueString(const UniqueString& other) 
        : length_(other.length_), data_(new char[other.length_ + 1]) {
        strcpy(data_, other.data_);
    }
    
    // 3. Move constructor
    UniqueString(UniqueString&& other) noexcept 
        : data_(other.data_), length_(other.length_) {
        other.data_ = nullptr;
        other.length_ = 0;
    }
    
    // 4 & 5. Unified assignment (copy-and-swap)
    UniqueString& operator=(UniqueString other) noexcept {
        std::swap(data_, other.data_);
        std::swap(length_, other.length_);
        return *this;
    }
    
    const char* c_str() const { return data_ ? data_ : ""; }
};

int main() {
    UniqueString s1("Hello");
    UniqueString s2 = s1;              // Copy
    UniqueString s3 = std::move(s1);   // Move
    s2 = UniqueString("World");        // Move assign
    
    std::cout << "s1: " << s1.c_str() << std::endl;  // empty
    std::cout << "s2: " << s2.c_str() << std::endl;  // World
    std::cout << "s3: " << s3.c_str() << std::endl;  // Hello
}
04

Perfect Forwarding

Perfect forwarding preserves the value category (lvalue or rvalue) of arguments when passing them to other functions. This is essential for writing generic wrapper functions and factories.

The Problem: Losing Value Category

Imagine you want to write a wrapper function that calls another function with some arguments. You want your wrapper to pass those arguments exactly as they were received - if an lvalue came in, pass it as an lvalue; if an rvalue came in, pass it as an rvalue.

But there's a problem: parameters with names are always lvalues, even if they're declared as T&&! This is because any named variable has an identity and a location in memory, making it an lvalue by definition.

This means if you receive an rvalue reference parameter, it becomes an lvalue inside your function, and you'll accidentally trigger copies instead of moves when passing it along. This defeats the purpose of move semantics!

#include <iostream>
#include <string>

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

We set up two overloads of process - one for lvalues taking std::string& and one for rvalues taking std::string&&. These will help us visualize which version gets called when we pass arguments through our wrapper function. The key issue we're about to see is that even when we receive an rvalue in the wrapper, we lose that information when passing it along.

// Naive wrapper - BROKEN!
void wrapper(std::string&& arg) {
    // arg is a named variable, so it's an lvalue!
    process(arg);  // Always calls lvalue version
}

This wrapper function accepts an rvalue reference parameter arg, which seems correct. However, there's a critical problem: even though arg is declared as std::string&&, once it has a name, it becomes an lvalue! This is because arg persists throughout the function body and has a memory location. When we call process(arg), the lvalue version is always selected, defeating the purpose of having the rvalue reference in the first place.

int main() {
    std::string s = "hello";
    // wrapper(s);           // Won't compile - can't bind lvalue to &&
    wrapper(std::string("temp"));  // Calls lvalue version - WRONG!
    return 0;
}
// Output: lvalue (but we wanted rvalue!)

The test demonstrates the problem: we pass a temporary string std::string("temp") (an rvalue) to the wrapper, expecting it to forward as an rvalue and call the rvalue version of process. Instead, the output shows "lvalue" - the wrapper accidentally converted our rvalue to an lvalue! This is the fundamental problem that perfect forwarding solves. Note that we can't even pass s (an lvalue) because the wrapper only accepts rvalue references.

Forwarding References (Universal References)

Here's where C++ gets clever. There's a special kind of reference called a forwarding reference (also known as a universal reference in older C++ literature). It looks like an rvalue reference (T&&), but it has magical properties!

A forwarding reference can bind to both lvalues and rvalues. But there's a catch: it only works this way when T is a template parameter being deduced in that specific context. The form must be exactly T&& with no qualifiers.

Think of it as a "universal adapter" - it accepts any type of value and remembers what kind it was (lvalue or rvalue). This is the foundation of perfect forwarding.

T&& is a forwarding reference ONLY when:
  1. T is a template parameter being deduced in that context
  2. The form is exactly T&& (not const T&& or vector<T>&&)
#include <iostream>

// T&& here is a FORWARDING REFERENCE
template<typename T>
void universal(T&& arg) {
    // T is deduced, so T&& can bind to anything
}

// NOT a forwarding reference - T is not being deduced here
template<typename T>
class Container {
    void add(T&& item);  // This is just an rvalue reference to T
};

Here we see the crucial distinction: universal uses a forwarding reference because T is being deduced at the call site, so T&& can bind to both lvalues and rvalues. In contrast, Container::add does NOT use a forwarding reference - T is fixed when the Container is instantiated, so T&& is just a regular rvalue reference. The magic only happens when template type deduction occurs for that specific parameter.

int main() {
    int x = 10;
    
    universal(x);      // T = int&,  T&& = int& (lvalue)
    universal(42);     // T = int,   T&& = int&& (rvalue)
    
    // Reference collapsing rules:
    // T& &   -> T&
    // T& &&  -> T&
    // T&& &  -> T&
    // T&& && -> T&&
    
    return 0;
}

The magic unfolds: when we pass the lvalue x, the compiler deduces T = int&, so T&& becomes int& &&, which collapses to int& - an lvalue reference! When we pass the rvalue 42, the compiler deduces T = int, so T&& is int&& - an rvalue reference. The reference collapsing rules shown in the comments explain how double references collapse: any reference with an & collapses to &, and only && && stays as &&.

Reference Collapsing Rules

You might be wondering: how can T&& bind to both lvalues and rvalues? The answer lies in a subtle C++ mechanism called reference collapsing.

Normally, you can't have a "reference to a reference" in C++ - syntax like int& & is illegal. However, during template type deduction, the compiler might temporarily create such types internally, and it needs rules to "collapse" them into a single reference type.

Here's how it works: when you pass an lvalue to a forwarding reference template, the template parameter T deduces to a reference type (e.g., int&). Then when you apply && to that, you get int& &&, which collapses to int&. This is why forwarding references can accept lvalues!

When references combine (like T& &&), C++ uses reference collapsing rules:

If T is... T&& becomes... Result
int int&& rvalue ref
int& int& && = int& lvalue ref
int&& int&& && = int&& rvalue ref
Simple Rule: Any reference with an & in it collapses to &. Only && && stays as &&.

std::forward - The Perfect Forwarding Tool

Now we come to the star of perfect forwarding: std::forward. While std::move unconditionally casts to an rvalue, std::forward is conditional - it only casts to an rvalue if the original argument was an rvalue.

Here's how it works: std::forward<T>(arg) looks at the template parameter T to determine what the original argument type was. If T is a reference type (meaning an lvalue was passed), std::forward returns an lvalue reference. If T is not a reference (meaning an rvalue was passed), it returns an rvalue reference.

In other words, std::forward preserves the original value category:

  • If the original was an lvalue, forward returns an lvalue reference
  • If the original was an rvalue, forward returns an rvalue reference
#include <iostream>
#include <string>
#include <utility>

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

void process(std::string&& s) { 
    std::cout << "rvalue: " << s << std::endl; 
}

We define our test bed with two overloads: one for lvalues taking const std::string& and one for rvalues taking std::string&&. These will demonstrate whether std::forward correctly preserves the value category of arguments passed through the wrapper. When the wrapper forwards an lvalue, we should see "lvalue" printed; when it forwards an rvalue, we should see "rvalue".

// Perfect forwarding wrapper
template<typename T>
void wrapper(T&& arg) {
    // std::forward preserves the original value category
    process(std::forward<T>(arg));
}

This is the perfect forwarding pattern! The wrapper function takes a forwarding reference T&&, which can bind to both lvalues and rvalues. The key is std::forward<T>(arg) - it examines the template parameter T to determine what the original argument was. If T is a reference type (lvalue was passed), forward returns an lvalue reference. If T is not a reference (rvalue was passed), forward returns an rvalue reference. This conditional casting preserves the original value category.

int main() {
    std::string s = "hello";
    
    wrapper(s);                      // Forwards as lvalue
    wrapper(std::string("world"));   // Forwards as rvalue
    wrapper("literal");              // Forwards as rvalue (decays to temp)
    
    return 0;
}
// Output:
// lvalue: hello
// rvalue: world
// rvalue: literal

Perfect forwarding in action! When we pass s (an lvalue), the wrapper forwards it as an lvalue, calling the lvalue version of process. When we pass std::string("world") (an explicit temporary) or "literal" (which creates a temporary), the wrapper forwards them as rvalues. The output confirms that std::forward perfectly preserved each argument's value category - exactly what we need for efficient generic code!

std::move vs std::forward

std::move(arg)

Always casts to rvalue reference

  • Use when you know you want to move
  • No template parameter needed
  • Unconditional cast to rvalue
std::forward<T>(arg)

Conditionally casts based on T

  • Use in templates with forwarding refs
  • Requires template parameter T
  • Preserves original value category

Real-World Example: Factory Function

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

class Widget {
public:
    Widget(int id, std::string name) 
        : id_(id), name_(std::move(name)) {
        std::cout << "Widget created: " << name_ << std::endl;
    }
private:
    int id_;
    std::string name_;
};

The Widget class demonstrates a typical resource-owning object that benefits from move semantics. Its constructor takes a std::string by value and uses std::move to transfer it to the member variable name_. This idiom allows the constructor to accept both lvalues (which will be copied into the parameter) and rvalues (which will be moved into the parameter), then always moves from the parameter to the member.

// Perfect forwarding factory function
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)...)
    );
}

This is a classic perfect forwarding factory pattern (similar to std::make_unique). The variadic template Args&&... accepts any number of arguments of any type, and std::forward<Args>(args)... perfectly forwards each argument to the constructor of T. The ellipsis ... expands the parameter pack, applying std::forward to each argument individually. This allows the factory to construct objects with maximum efficiency, copying when necessary and moving when possible.

int main() {
    std::string name = "MyWidget";
    
    // name is forwarded as lvalue (will be copied in Widget)
    auto w1 = make_unique_custom<Widget>(1, name);
    std::cout << "name after w1: " << name << std::endl;
    
    // std::move(name) is forwarded as rvalue (will be moved in Widget)
    auto w2 = make_unique_custom<Widget>(2, std::move(name));
    std::cout << "name after w2: " << name << std::endl;  // Empty!
    
    // Literal is forwarded as rvalue
    auto w3 = make_unique_custom<Widget>(3, "Temporary");
    
    return 0;
}
// Output:
// Widget created: MyWidget
// name after w1: MyWidget
// Widget created: MyWidget
// name after w2: 
// Widget created: Temporary

The test demonstrates perfect forwarding's efficiency. When we pass name directly, it's forwarded as an lvalue and copied into the Widget - name remains valid afterward. When we pass std::move(name), it's forwarded as an rvalue and moved into the Widget - name is left empty. The string literal creates a temporary and is also forwarded as an rvalue. Perfect forwarding ensures each argument is handled optimally: copies when we need the original, moves when we don't.

Common Perfect Forwarding Patterns

// Pattern 1: Wrapper function
template<typename Func, typename... Args>
auto invoke_and_log(Func&& f, Args&&... args) {
    std::cout << "Calling function..." << std::endl;
    return std::forward<Func>(f)(std::forward<Args>(args)...);
}

The wrapper function pattern is used when you want to intercept function calls (for logging, timing, caching, etc.) while preserving the exact behavior of the original call. Here, invoke_and_log forwards both the function object f and all its arguments args using perfect forwarding. This ensures that lvalue arguments remain lvalues and rvalue arguments remain rvalues when passed to the actual function, maintaining optimal performance and correct semantics.

// Pattern 2: Emplace-style function
template<typename T>
class Container {
    std::vector<T> data_;
public:
    template<typename... Args>
    void emplace(Args&&... args) {
        data_.emplace_back(std::forward<Args>(args)...);
    }
};

The emplace pattern constructs objects directly in the container's storage, avoiding unnecessary copies or moves. Instead of creating a temporary T and then moving it into the container, emplace forwards the constructor arguments directly to the container's internal storage where the object is constructed in-place. This is exactly how std::vector::emplace_back and similar STL container methods work, providing maximum efficiency when adding elements.

// Pattern 3: Constructor forwarding
class MyClass {
    std::string value_;
public:
    template<typename T>
    explicit MyClass(T&& val) 
        : value_(std::forward<T>(val)) {}
};

The constructor forwarding pattern allows a class to accept any type that's convertible to its member type while preserving move semantics. The forwarding reference T&& accepts both lvalues and rvalues of compatible types, and std::forward<T>(val) ensures the argument is moved when possible (rvalue) or copied when necessary (lvalue). This single templated constructor handles all cases efficiently without needing separate copy and move constructors for the wrapping class.

Practice Questions: Perfect Forwarding

Given:

void target(int& x) { std::cout << "lvalue: " << x << std::endl; }
void target(int&& x) { std::cout << "rvalue: " << x << std::endl; }

Task: Write a template function wrapper that uses perfect forwarding to call target, preserving the value category of its argument. Test with both an lvalue and an rvalue.

Expected output: lvalue: 10 then rvalue: 20

Show Solution
#include <iostream>
#include <utility>

void target(int& x) { std::cout << "lvalue: " << x << std::endl; }
void target(int&& x) { std::cout << "rvalue: " << x << std::endl; }

template<typename T>
void wrapper(T&& arg) {
    target(std::forward<T>(arg));
}

int main() {
    int x = 10;
    wrapper(x);   // lvalue: 10
    wrapper(20);  // rvalue: 20
}

Given:

class Widget {
public:
    Widget(int id, const std::string& name) {
        std::cout << "Widget(" << id << ", " << name << ")" << std::endl;
    }
    Widget(int id, std::string&& name) {
        std::cout << "Widget(" << id << ", moved " << name << ")" << std::endl;
    }
};

Task: Write a variadic template factory function createWidget that perfectly forwards all arguments to the Widget constructor. Test with both lvalue and rvalue string arguments.

Expected: Calling with lvalue string should copy, calling with rvalue should move.

Show Solution
#include <iostream>
#include <string>
#include <utility>

class Widget {
public:
    Widget(int id, const std::string& name) {
        std::cout << "Widget(" << id << ", " << name << ")" << std::endl;
    }
    Widget(int id, std::string&& name) {
        std::cout << "Widget(" << id << ", moved " << name << ")" << std::endl;
    }
};

template<typename... Args>
Widget createWidget(Args&&... args) {
    return Widget(std::forward<Args>(args)...);
}

int main() {
    std::string name = "Gadget";
    
    auto w1 = createWidget(1, name);              // Copies
    auto w2 = createWidget(2, std::string("Gizmo")); // Moves
    // Output:
    // Widget(1, Gadget)
    // Widget(2, moved Gizmo)
}

Given:

struct Person {
    std::string name;
    int age;
    Person(std::string n, int a) : name(std::move(n)), age(a) {
        std::cout << "Constructed: " << name << std::endl;
    }
};

class PersonList {
    std::vector<Person> people_;
public:
    // TODO: Add emplace method
    void print() const {
        for (const auto& p : people_)
            std::cout << p.name << " (" << p.age << ")" << std::endl;
    }
};

Task: Add an emplace method to PersonList that constructs a Person in-place using perfect forwarding. Avoid creating temporary Person objects.

Hint: Use variadic templates with std::forward and emplace_back.

Show Solution
#include <iostream>
#include <string>
#include <vector>
#include <utility>

struct Person {
    std::string name;
    int age;
    Person(std::string n, int a) : name(std::move(n)), age(a) {
        std::cout << "Constructed: " << name << std::endl;
    }
};

class PersonList {
    std::vector<Person> people_;
public:
    template<typename... Args>
    void emplace(Args&&... args) {
        people_.emplace_back(std::forward<Args>(args)...);
    }
    
    void print() const {
        for (const auto& p : people_)
            std::cout << p.name << " (" << p.age << ")" << std::endl;
    }
};

int main() {
    PersonList list;
    
    std::string name = "Alice";
    list.emplace(name, 30);           // Copies name
    list.emplace("Bob", 25);          // Constructs in-place
    list.emplace(std::move(name), 35); // Moves name
    
    list.print();
}

Interactive Demo: Value Category Classifier

Test your understanding! Enter a C++ expression and see if you can correctly identify its value category before revealing the answer.

Classify the Expression
x

Given: int x = 10;

0 Correct 0 Wrong

Key Takeaways

Lvalues Have Identity

Lvalues have names and addresses you can take. Rvalues are temporaries about to be destroyed

&& Binds to Temporaries

Rvalue references (&&) can bind to rvalues, enabling you to detect temporary objects

Move Instead of Copy

Move constructors transfer resources, leaving the source in a valid but empty state

std::move Enables Moving

Use std::move() to cast an lvalue to an rvalue, allowing move operations on it

std::forward Preserves

Use std::forward in templates to preserve the original value category of arguments

Performance Gains

Move semantics can provide massive speedups, especially for containers and large objects

Knowledge Check

Quick Quiz

Test what you've learned about rvalue references and move semantics

1 Which of the following is an rvalue?
2 What does std::move() actually do?
3 After std::move(str) where str is a string, what is str's state?
4 What is the purpose of std::forward<T>()?
5 Which is the correct signature for a move constructor?
6 When should you NOT use std::move()?
Answer all questions to check your score