Advanced 14 min

Rvalue References

Master rvalue references to enable move semantics and eliminate unnecessary copying

Understand rvalue references and move semantics - the foundation of efficient modern C++ that eliminates expensive copying.

A Simple Example

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

class Buffer {
    size_t* data;
    size_t size;

public:
    Buffer(size_t s) : size{s} {
        data = new size_t[size];
        std::cout << "Buffer created (" << size << " elements)\n";
    }

    // Copy constructor - expensive
    Buffer(const Buffer& other) : size{other.size} {
        data = new size_t[size];
        std::copy(other.data, other.data + size, data);
        std::cout << "Buffer copied (" << size << " elements)\n";
    }

    // Move constructor - cheap
    Buffer(Buffer&& other) noexcept : data{other.data}, size{other.size} {
        other.data = nullptr;
        other.size = 0;
        std::cout << "Buffer moved (" << size << " elements)\n";
    }

    ~Buffer() {
        delete[] data;
        std::cout << "Buffer destroyed\n";
    }
};

Buffer createBuffer() {
    return Buffer{1000000};  // Returns temporary - move, not copy
}

int main() {
    std::cout << "=== Creating buffer ===\n";
    Buffer buf1{createBuffer()};  // Move, not copy!

    std::cout << "\n=== Moving buffer ===\n";
    Buffer buf2{std::move(buf1)};  // Explicit move

    return 0;
}

Breaking It Down

Lvalues vs Rvalues

  • Lvalue: Has a name and address. Can appear on left side of assignment. Example: int x = 5; (x is an lvalue)
  • Rvalue: Temporary value without a name. Cannot take its address. Example: 5, x + y, function return values
  • Think of it: Lvalues are nouns (things), rvalues are verbs or expressions (actions)
  • Remember: If you can take its address with &, it is an lvalue

Rvalue References &&

  • Syntax: T&& binds to rvalues (temporaries)
  • Purpose: Enables move semantics - transfer ownership instead of copying
  • Benefit: Avoid expensive deep copies for large objects
  • Remember: Regular references T& bind to lvalues, T&& bind to rvalues

Move Constructor

  • Signature: ClassName(ClassName&& other) noexcept
  • What it does: Steals resources from the source object and nulls it out
  • Performance: O(1) pointer swap vs O(n) deep copy
  • Remember: Always mark move constructors noexcept for optimal performance

std::move() - Explicit Move

  • What it does: Casts an lvalue to an rvalue reference, enabling move
  • Usage: Buffer b2{std::move(b1)}; - explicitly request move
  • Warning: After std::move(x), x is in a valid but unspecified state - do not use it
  • Remember: std::move does not actually move anything - it just enables moving

Why This Matters

  • Copying large objects like vectors or strings is expensive - sometimes 100x slower than necessary.
  • Move semantics let you transfer ownership of resources instead of copying them, dramatically improving performance.
  • Understanding lvalues vs rvalues helps you write efficient code that avoids hidden performance costs.
  • Modern C++ relies heavily on move semantics - it powers perfect forwarding, smart pointers, and standard containers.

Critical Insight

Move semantics is like transferring ownership of a house. Copying is like building an exact duplicate house next door (expensive!). Moving is like handing over the keys and title - the house stays where it is, you just transfer ownership (cheap!).

When you return a large object from a function, the compiler automatically moves it instead of copying it. This is why modern C++ code can return vectors and strings by value without performance penalties.

Best Practices

Always mark move constructors and move assignment operators noexcept: This enables optimizations in STL containers.

After std::move(), treat the moved-from object as if it no longer exists: Do not access its state except to assign a new value.

Implement move semantics for classes that manage resources: If your class owns heap memory, file handles, or other resources, add move constructor and move assignment.

Return by value for large objects: The compiler automatically uses move semantics, making it efficient.

Common Mistakes

Using an object after std::move(): After moving from an object, it is in an unspecified state. Reassign it before using it.

Forgetting noexcept: Without noexcept, STL containers will not use your move operations and will fall back to copying.

Thinking std::move() actually moves: std::move() only casts to rvalue reference. The actual move happens in the move constructor or move assignment operator.

Not nulling out pointers in move constructor: After transferring ownership, set source pointers to nullptr to prevent double deletion.

Debug Challenge

This code tries to transfer ownership but accidentally copies. Click the highlighted line to fix it:

1 #include <vector>
2
3 int main() {
4 std::vector<int> v1{1, 2, 3, 4, 5};
5 std::vector<int> v2{v1};
6 // v1 should be empty now
7 return 0;
8 }

Quick Quiz

  1. What happens after you call std::move(x)?
x is in a valid but unspecified state - do not use it
x is unchanged
x is deleted from memory
  1. Why should move constructors be marked noexcept?
noexcept enables optimizations in containers like std::vector
noexcept is required by the compiler
noexcept makes the code run faster at runtime
  1. Which is an rvalue?
The result of x + y
A variable x
A parameter passed by reference

Practice Playground

Time to try out what you just learned! Play with the example code below, experiment by making changes and running the code to deepen your understanding.

Lesson Progress

  • Fix This Code
  • Quick Quiz
  • Practice Playground - run once