Skip to content

Perfect Forwarding & std::forward

Understand why named rvalue references are lvalues, learn the canonical forwarding pattern with std::forward, and see how emplace_back uses variadic forwarding to construct objects in-place.

The Problem: Named Rvalue References Are Lvalues

Here's the fundamental problem perfect forwarding solves: when you accept an argument as T&& and pass it to another function, the name makes it an lvalue:

```cpp
void inner(int& x) { std::cout << "lvalue\n"; }
void inner(int&& x) { std::cout << "rvalue\n"; }

void outer(int&& x) {
inner(x); // calls inner(int&) — x is an lvalue!
}
```

The value category is lost at every function boundary. If you are writing a wrapper function (logging, timing, factory), you need to forward the argument with its original value category preserved. Simply passing x always passes an lvalue. Using std::move(x) always passes an rvalue. Neither is correct for a generic wrapper.

std::forward: The Conditional Cast

std::forward(x) is a conditional cast:

- If T is an lvalue reference (deduced because an lvalue was passed), std::forward(x) returns an lvalue reference — no cast.
- If T is a non-reference (deduced because an rvalue was passed), std::forward(x) returns an rvalue reference — casts to T&&.

The key difference from std::move:
- std::move always casts to rvalue (unconditional).
- std::forward casts to rvalue only if the original argument was an rvalue (conditional).

std::forward should ONLY be used with forwarding references (T&& where T is deduced). Using it elsewhere is a bug.

The Canonical Forwarding Pattern

The pattern: accept a forwarding reference T&&, then use std::forward(arg) to preserve the value category when passing to another function.

perfect_forwarding.cpp
#include <iostream>
#include <string>
#include <utility>
#include <chrono>

// The function we want to wrap
void process(const std::string& s) {
    std::cout << "process(lvalue): " << s << '\n';
}
void process(std::string&& s) {
    std::cout << "process(rvalue): " << s << '\n';
    // Can steal s's resources since it's an rvalue
}

// Generic wrapper that perfectly forwards
template <typename T>
void timed_process(T&& arg) {
    auto start = std::chrono::steady_clock::now();

    // std::forward<T>(arg) preserves the original value category
    process(std::forward<T>(arg));

    auto elapsed = std::chrono::steady_clock::now() - start;
    std::cout << "Took: " << elapsed.count() << " ns\n";
}

int main() {
    std::string name = "Alice";
    timed_process(name);              // T = string&,  forwards as lvalue
    timed_process(std::string("Bob")); // T = string, forwards as rvalue
    timed_process("Charlie");          // T = const char(&)[8], forwards as lvalue
}

Variadic Forwarding & emplace_back

Perfect forwarding truly shines with variadic templates. emplace_back forwards constructor arguments directly into the container, constructing the object in-place without any copies or moves.

variadic_forwarding.cpp
#include <iostream>
#include <vector>
#include <string>
#include <utility>

struct Employee {
    std::string name;
    int age;
    Employee(std::string n, int a) : name(std::move(n)), age(a) {
        std::cout << "Constructed: " << name << '\n';
    }
    Employee(const Employee&) { std::cout << "Copied\n"; }
    Employee(Employee&&) noexcept { std::cout << "Moved\n"; }
};

// Custom factory using variadic forwarding
template <typename T, typename... Args>
std::vector<T> make_vector_of(Args&&... args) {
    std::vector<T> v;
    // Fold expression (C++17) + emplace_back
    (v.emplace_back(std::forward<Args>(args)), ...);
    return v;
}

int main() {
    std::vector<Employee> team;

    // push_back: creates temporary, then MOVES it into vector
    team.push_back(Employee("Alice", 30));
    // Output: Constructed: Alice, Moved

    // emplace_back: constructs IN-PLACE — no copy, no move
    team.emplace_back("Bob", 25);
    // Output: Constructed: Bob

    std::cout << "---\n";
    // Variadic forwarding
    auto v = make_vector_of<std::string>("hello", std::string("world"), "!");
    for (const auto& s : v) std::cout << s << ' ';
    std::cout << '\n';
}

move vs forward: Know the Difference

| | std::move | std::forward |
|---|---|---|
| Cast type | Unconditional cast to T&& | Conditional — lvalue if T is T&, rvalue if T is T |
| Use case | "I'm done with this, steal it" | "Pass this along with its original value category" |
| Context | Concrete types, last use of a variable | Template forwarding references only |
| Danger | Accidentally moving a value you still need | Using without a forwarding reference (meaningless) |

Rule of thumb: use std::move when you own the value and want to give it away. Use std::forward when you are forwarding someone else's value.

Pitfall

1. Forwarding more than once: std::forward(arg) may cast to rvalue. If you forward the same argument to two functions, the first may steal its resources, leaving garbage for the second. Forward each argument at most once.

2. auto&& is a forwarding reference: for (auto&& elem : container) deduces the value category. This is fine for range-for but be careful when using auto&& in other contexts.

3. std::forward without deduction: std::forward(s) is just std::move(s) in disguise. Only use std::forward with a deduced template parameter.

4. Braced-init-lists: {1, 2, 3} has no type and cannot be forwarded. Assign to a variable first.

Best Practice

1. Use std::forward only with forwarding references (T&& where T is deduced).
2. Forward each argument at most once — forwarding may move, leaving the source empty.
3. Prefer emplace_back over push_back when constructing objects in containers.
4. Don't forward in a loop — the first iteration may steal the argument.
5. Use if constexpr with std::is_lvalue_reference_v when you need different behavior for lvalues and rvalues.

Key Takeaways
  • Named rvalue references are lvalues — the value category is lost at function boundaries
  • std::forward conditionally casts: lvalue stays lvalue, rvalue stays rvalue
  • std::move is unconditional (always rvalue); std::forward is conditional (preserves original)
  • emplace_back uses variadic forwarding to construct objects in-place with zero copies
  • Forward each argument at most once — forwarding may steal resources

Quiz — Test Your Knowledge

(20 XP)

1. What is the difference between `std::move` and `std::forward`?

2. Why is `emplace_back` more efficient than `push_back` for constructing elements?

3. What happens if you `std::forward` the same argument to two different functions?