πŸ”’ Private Site

This site is password-protected.

Move Semantics & Perfect Forwarding

Move semantics (C++11) is one of the most important performance features in modern C++. It eliminates unnecessary copies by transferring resources instead of duplicating them.


Table of Contents

1 β€” The Problem: Unnecessary Copies

2 β€” Value Categories

3 β€” Move Constructor & Move Assignment

4 β€” std::move

5 β€” Rule of Five & Rule of Zero

6 β€” noexcept

7 β€” Perfect Forwarding

8 β€” Copy/Move Elision (RVO/NRVO)

9 β€” Summary Table

Practice Questions


Glossary β€” Key Terms at a Glance

Term Meaning
lvalue Expression with identity (has an address) β€” named variables
rvalue Temporary/expiring value β€” no persistent address
Move constructor Steals resources from an rvalue β€” O(1)
std::move Cast to rvalue β€” tells compiler β€œI’m done with this”
std::forward Conditional cast β€” preserves lvalue/rvalue-ness in templates
Universal reference T&& in template context β€” binds to both lvalues and rvalues
Rule of Five If you define one of destructor/copy/move, define all five
Rule of Zero Use RAII types so you don’t need to define any of the five
RVO/NRVO Compiler eliminates copy/move when returning by value
noexcept Promise that a function won’t throw β€” required for STL optimizations

1 β€” The Problem: Unnecessary Copies

1.1 Why Copies Hurt

std::vector<int> createLargeVector() {
    std::vector<int> v(1'000'000);
    // fill v...
    return v;  // Pre-C++11: COPIES entire vector to caller!
}

std::vector<int> result = createLargeVector();
// Without move semantics: 1 million ints copied needlessly

⚠️ Problem: Before C++11, returning large objects by value meant deep copying all their data. Move semantics solves this by transferring ownership of the internal resources (pointers, buffers) instead.


2 β€” Value Categories

2.1 lvalue vs rvalue

Definition: Every expression in C++ is either an lvalue (has identity, has address) or an rvalue (temporary, no persistent address).

int x = 42;        // x is an lvalue (has identity, has address)
                    // 42 is an rvalue (temporary, no address)

int& ref = x;       // OK: lvalue ref binds to lvalue
// int& ref = 42;   // ERROR: lvalue ref cannot bind to rvalue

int&& rref = 42;    // OK: rvalue ref binds to rvalue
// int&& rref = x;  // ERROR: rvalue ref cannot bind to lvalue
Expression Type Example
Named variable lvalue x, arr[0], *ptr
Temporary rvalue 42, x + y, std::string("hi")
Function return value rvalue getVector()
std::move(x) rvalue (cast) Converts lvalue to rvalue

3 β€” Move Constructor & Move Assignment

3.1 Implementing Move Operations

class Buffer {
    int* data;
    size_t size;
    
public:
    // Constructor
    Buffer(size_t n) : data(new int[n]), size(n) {}
    
    // Destructor
    ~Buffer() { delete[] data; }
    
    // Copy constructor β€” EXPENSIVE (deep copy)
    Buffer(const Buffer& other) : data(new int[other.size]), size(other.size) {
        std::copy(other.data, other.data + size, data);
    }
    
    // Copy assignment
    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);
        }
        return *this;
    }
    
    // Move constructor β€” CHEAP (steal resources)
    Buffer(Buffer&& other) noexcept
        : data(other.data), size(other.size) {
        other.data = nullptr;   // leave source in valid state
        other.size = 0;
    }
    
    // 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;
    }
};

πŸ” Why This Logic: The move constructor simply copies the pointer and size, then nullifies the source. No allocation, no memcpy. The source is left in a valid-but-empty state.

Copy vs Move Performance:

Copy: allocate new memory β†’ copy all elements β†’ O(n)
Move: swap 2 pointers + 1 size_t              β†’ O(1)

For a vector with 1 million elements:

  • Copy: ~4MB allocation + memcpy
  • Move: 3 pointer/int swaps (~24 bytes)

4 β€” std::move

4.1 When to Use std::move

Definition: std::move doesn’t move anything. It’s a cast that says β€œI’m done with this, you can take its resources.” The actual move happens in the move constructor/assignment.

std::string s1 = "Hello World";
std::string s2 = std::move(s1);  // s1's buffer transferred to s2

// s1 is now in a "valid but unspecified state"
// Only safe operations on moved-from objects: assign or destroy
std::cout << s2;    // "Hello World"
s1 = "Reused";      // OK: can assign after move

When to use std::move:

// 1. Passing objects you no longer need
std::vector<int> v{1, 2, 3, 4, 5};
processData(std::move(v));  // avoids copy β€” v is moved into parameter

// 2. Storing in containers
std::vector<std::string> names;
std::string name = "Alice";
names.push_back(std::move(name));  // move instead of copy

// 3. Moving into member variables
class Widget {
    std::string name;
public:
    Widget(std::string n) : name(std::move(n)) {}
    // Parameter n is copied/moved into function, then moved into member
};

4.2 When NOT to Use std::move

⚠️ Common anti-patterns:

// DON'T: move from const objects (no effect β€” falls back to copy)
const std::string s = "hello";
auto s2 = std::move(s);  // COPIES, not moves! const prevents move.

// DON'T: move return values (prevents RVO)
std::string bad() {
    std::string s = "hello";
    return std::move(s);  // WRONG β€” prevents compiler optimization!
}
std::string good() {
    std::string s = "hello";
    return s;  // RIGHT β€” compiler applies RVO (even better than move)
}

// DON'T: move trivial types (int, double, pointers)
int x = 42;
int y = std::move(x);  // Pointless β€” ints don't have resources to transfer

5 β€” Rule of Five & Rule of Zero

5.1 Special Member Functions

Rule of Five: If you define ANY of these, you should define ALL of them:

Special Member Signature
Destructor ~T()
Copy constructor T(const T&)
Copy assignment T& operator=(const T&)
Move constructor T(T&&) noexcept
Move assignment T& operator=(T&&) noexcept

Rule of Zero (PREFERRED): Don’t manage resources manually. Use smart pointers and standard containers:

class Person {
    std::string name;
    std::unique_ptr<int[]> scores;
    std::vector<std::string> friends;
    // No need to write any of the 5 special members!
    // std::string, unique_ptr, vector all handle their own memory.
};

= default and = delete:

class Widget {
public:
    Widget() = default;              // compiler-generated default
    Widget(const Widget&) = delete;  // prevent copying
    Widget& operator=(const Widget&) = delete;
    Widget(Widget&&) = default;      // compiler-generated move
    Widget& operator=(Widget&&) = default;
};

6 β€” noexcept

6.1 Critical for Move Operations

⚠️ Always mark move operations noexcept. Without it, STL containers fall back to copying for exception safety.

class Buffer {
public:
    Buffer(Buffer&& other) noexcept;             // MUST be noexcept
    Buffer& operator=(Buffer&& other) noexcept;  // MUST be noexcept
};

// WHY: std::vector::push_back uses move ONLY if move constructor is noexcept
// If not noexcept, it copies (to maintain strong exception guarantee)

7 β€” Perfect Forwarding

7.1 Universal References & std::forward

Problem: How to write a wrapper that passes arguments along exactly as they were received (preserving lvalue/rvalue-ness)?

The Problem:

template<typename T>
void wrapper(T arg) {
    process(arg);  // ALWAYS calls lvalue version β€” arg is named, so it's an lvalue!
}
wrapper(42);  // 42 is rvalue, but process gets an lvalue

The Solution β€” Universal References + std::forward:

template<typename T>
void wrapper(T&& arg) {  // && with template = UNIVERSAL REFERENCE
    process(std::forward<T>(arg));  // forwards as-is
}

std::string s = "hello";
wrapper(s);              // T = string&, forwards as lvalue
wrapper(std::string("hi")); // T = string, forwards as rvalue

Reference Collapsing Rules:

T& &   β†’ T&     (lvalue ref to lvalue ref β†’ lvalue ref)
T& &&  β†’ T&     (rvalue ref to lvalue ref β†’ lvalue ref)
T&& &  β†’ T&     (lvalue ref to rvalue ref β†’ lvalue ref)
T&& && β†’ T&&    (rvalue ref to rvalue ref β†’ rvalue ref)

πŸ” Why This Logic: std::forward is a conditional cast. If the original argument was an lvalue, it forwards as lvalue. If rvalue, it forwards as rvalue. This preserves the caller’s intent perfectly.


7.2 Factory Pattern & emplace_back

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)...));
}

auto p = make_unique_custom<Widget>(42, "hello", 3.14);

emplace_back Uses Perfect Forwarding:

std::vector<std::pair<std::string, int>> v;

// push_back: construct pair, then copy/move into vector
v.push_back(std::make_pair("alice", 90));

// emplace_back: construct pair IN PLACE inside vector
v.emplace_back("alice", 90);  // forwards args to pair's constructor β€” no copies!

8 β€” Copy/Move Elision (RVO/NRVO)

8.1 Return Value Optimization

Definition: The compiler can eliminate copy/move entirely β€” constructing the object directly in the caller’s stack frame.

std::string createString() {
    return std::string("hello");  // RVO: Return Value Optimization
    // Object constructed directly in caller's stack frame
}

std::string createNamed() {
    std::string s = "hello";
    return s;  // NRVO: Named Return Value Optimization
}

std::string result = createString();  // NO copy, NO move β€” zero overhead

C++17 guarantees RVO (mandatory copy elision) for returning prvalues. NRVO is optional but nearly all compilers implement it. Never return std::move(x) β€” it prevents these optimizations!


9 β€” Summary Table

9.1 When Things Are Copied vs Moved

Situation What Happens
Return local by value RVO (elided)
Return with std::move Move (worse!)
Pass lvalue to T param Copy
Pass std::move(lv) to T Move
Pass temporary to T param Move
push_back(lvalue) Copy
push_back(std::move(lv)) Move
emplace_back(args...) Construct in-place

Practice Questions

Q1. Explain the difference between an lvalue and an rvalue. Give 3 examples of each.

Q2. Write a complete class DynamicArray with all Rule of Five members (destructor, copy/move constructor, copy/move assignment). Test that moves are O(1).

Q3. What does std::move actually do? Why doesn’t it β€œmove” anything?

Q4. Why should you NEVER write return std::move(x) from a function? What optimization does it prevent?

Q5. Explain noexcept for move operations. What happens to std::vector::push_back if the move constructor is not noexcept?

Q6. What is the Rule of Zero? Rewrite a class that currently uses Rule of Five to follow Rule of Zero instead.

Q7. Explain universal references (T&& in template context). How do they differ from rvalue references?

Q8. What does std::forward do? Write a function that demonstrates perfect forwarding.

Q9. Explain reference collapsing rules. What does T& && collapse to? Why?

Q10. Compare push_back(std::move(x)) vs emplace_back(args...). When is each appropriate?


Key Takeaways

  1. Move semantics transfer resources instead of copying β€” O(1) instead of O(n)
  2. std::move is just a cast β€” it doesn’t move anything itself
  3. Don’t move return values β€” let RVO/NRVO do its job
  4. Always mark moves noexcept β€” STL containers require it for optimization
  5. Use std::forward in templates for perfect forwarding
  6. emplace_back > push_back β€” constructs in place
  7. Rule of Zero β€” prefer it over Rule of Five
  8. Don’t use moved-from objects except to assign or destroy

← Back to C++ Notes