Skip to content

Perfect Forwarding — Forwarding References, std::forward, Reference Collapsing, Variadic Forwarding

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Perfect Forwarding. We cover key concepts, practical examples, and best practices to help you master this topic.

C++ perfect forwarding uses forwarding references (T&&) and std::forward to pass function arguments through wrapper functions while preserving their exact value category, const-ness, and reference-ness.

What You'll Learn

You will understand forwarding references (T&& in template context), apply std::forward to preserve value categories, implement variadic forwarding wrappers for Factory functions, use reference collapsing rules (T& + && → T&), and build generic delegates and functional wrappers.

Why It Matters

Without perfect forwarding, wrapper functions like make_unique, emplace_back, and std::bind would require overloads for every combination of lvalue and rvalue parameters. Perfect forwarding eliminates this explosion. Every C++ generic library uses it — mastering forwarding is essential for writing generic code that works with both copies and moves.

Learning Path

graph LR
    A["52: Move Semantics"] --> B["53: Perfect Forwarding"]
    B --> C["54: Structured Bindings"]
    C --> D["55: if/switch init + if constexpr"]
    style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
    style D fill:#4a90d9,stroke:#2c5f8a,color:#fff

The Problem: Why Forwarding is Needed

Without forwarding, wrappers force copies or lose value categories.

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

class Heavy {
    std::string data_;
public:
    Heavy(const std::string& s) : data_(s) {
        std::cout << "Constructed\n";
    }
    Heavy(const Heavy&) { std::cout << "Copied\n"; }
    Heavy(Heavy&&) noexcept { std::cout << "Moved\n"; }
};

// Bad wrapper: always copies
template <typename T>
Heavy badMakeHeavy(T arg) {
    return Heavy(arg);  // Always a copy of arg
}

// Better but wrong: always rvalue reference
template <typename T>
Heavy rvalueOnlyMakeHeavy(T&& arg) {
    // arg is an lvalue (it has a name)
    return Heavy(arg);  // Still copies! arg is an lvalue
}

int main() {
    std::string s = "very long string that would be expensive to copy";

    auto h1 = badMakeHeavy(s);             // Copies s
    auto h2 = badMakeHeavy(std::move(s));  // Copies s (move is ignored)

    // s is in unspecified state after move
}

Forwarding References

A T&& parameter in a template context is a forwarding reference (not an rvalue reference). It can bind to both lvalues and rvalues.

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

// T&& is a forwarding reference because T is deduced
template <typename T>
void showCategory(T&& arg) {
    if constexpr (std::is_lvalue_reference_v<T>) {
        std::cout << "Lvalue (T = " << typeid(T).name() << ")\n";
    } else {
        std::cout << "Rvalue\n";
    }
}

int main() {
    std::string s = "hello";

    showCategory(s);              // Lvalue (T = std::string&)
    showCategory(std::move(s));   // Rvalue (T = std::string)
    showCategory("temporary");    // Rvalue (T = const char (&)[10])

    // How it works:
    // When arg is an lvalue: T = T& → T&& = T& (reference collapsing)
    // When arg is an rvalue: T = T → T&& = T&&
}

Reference Collapsing Rules

C++ has four combinations of reference-to-reference:

Original Collapsed Rule
T& & T& Two lvalues → lvalue
T& && T& Lvalue + rvalue → lvalue
T&& & T& Rvalue + lvalue → lvalue
T&& && T&& Two rvalues → rvalue

Rule: If either is &, the result is &. Only T&& && collapses to &&.

#include <iostream>
#include <type_traits>

int main() {
    // Reference collapsing in action
    using LRef = int&;
    using RRef = int&&;

    // These are the collapsed types
    using A = LRef&;   // int&
    using B = LRef&&;  // int&
    using C = RRef&;   // int&
    using D = RRef&&;  // int&&

    std::cout << "A is int&: " << std::is_same_v<A, int&> << "\n";   // true
    std::cout << "B is int&: " << std::is_same_v<B, int&> << "\n";   // true
    std::cout << "C is int&: " << std::is_same_v<C, int&> << "\n";   // true
    std::cout << "D is int&&: " << std::is_same_v<D, int&&> << "\n"; // true
}

std::forward — Conditional Move

std::forward<T>(arg) casts arg back to the value category it originally had.

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

class Heavy {
public:
    Heavy(const std::string& s) { std::cout << "Constructed from lvalue\n"; }
    Heavy(std::string&& s) { std::cout << "Constructed from rvalue\n"; }
};

// Correct wrapper with perfect forwarding
template <typename T>
Heavy makeHeavy(T&& arg) {
    return Heavy(std::forward<T>(arg));
    // If arg was an lvalue: std::forward<T>(arg) → static_cast<T&>(arg) → lvalue
    // If arg was an rvalue: std::forward<T>(arg) → static_cast<T&&>(arg) → rvalue
}

int main() {
    std::string s = "test";

    auto h1 = makeHeavy(s);              // "Constructed from lvalue"
    auto h2 = makeHeavy(std::move(s));   // "Constructed from rvalue"
    auto h3 = makeHeavy("temporary");    // "Constructed from rvalue"
}

Variadic Perfect Forwarding

The most powerful use: forwarding any number of arguments with any types.

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

// Factory function that forwards all arguments
template <typename T, typename... Args>
std::unique_ptr<T> makeUnique(Args&&... args) {
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

class Person {
    std::string name_;
    int age_;
public:
    Person(std::string name, int age)
        : name_(std::move(name)), age_(age) {
        std::cout << "Person(" << name_ << ", " << age_ << ")\n";
    }
};

int main() {
    // Forward two arguments: string (rvalue) and int (prvalue)
    auto p = makeUnique<Person>("Alice", 30);

    // Forward via an lvalue
    std::string name = "Bob";
    auto p2 = makeUnique<Person>(name, 25);  // name is forwarded as lvalue

    // std::make_unique works exactly like this
    auto p3 = std::make_unique<Person>("Charlie", 35);
}

The pack expansion std::forward<Args>(args)... applies std::forward to each argument individually, preserving each one's value category.

Real-World: emplace_back Implementation

std::vector::emplace_back uses perfect forwarding to construct elements in place.

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

// Simplified emplace_back
template <typename T>
class SimpleVector {
    T* data_;
    size_t size_;
    size_t capacity_;

public:
    template <typename... Args>
    void emplace_back(Args&&... args) {
        if (size_ == capacity_) {
            // Reallocate (simplified — real implementation handles more)
            reserve(capacity_ * 2 + 1);
        }
        // Construct in place: forward args to T's constructor
        new (data_ + size_) T(std::forward<Args>(args)...);
        ++size_;
    }
};

int main() {
    std::vector<std::string> vec;

    // emplace_back forwards "hello" to string's constructor
    vec.emplace_back(5, 'h');        // Constructs "hhhhh" in place
    vec.emplace_back("hello");       // Constructs "hello" in place

    for (const auto& s : vec) {
        std::cout << s << " ";
    }
    std::cout << "\n";  // hhhhh hello

    // Without perfect forwarding, emplace_back couldn't forward arguments
    // It would need to copy pre-constructed objects (like push_back)
}

Forwarding Lambdas (C++20)

C++20 lambdas with explicit template parameters enable perfect forwarding in lambdas.

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

int main() {
    // C++14 generic lambda (does not forward perfectly)
    auto bad = [](auto&& x) {
        // x is lvalue (has name)
        // return someFunc(x);  // Always lvalue
        // return someFunc(std::forward<decltype(x)>(x));  // Correct
    };

    // C++20 forwarding lambda with explicit template parameter
    auto forwarder = []<typename T>(T&& x) {
        return std::forward<T>(x);
    };

    // Practical: make_unique wrapper as lambda
    auto make_unique_lambda = []<typename T, typename... Args>(Args&&... args) {
        return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
    };

    auto p = make_unique_lambda.operator()<std::pair<int, double>>(42, 3.14);
    std::cout << p->first << " " << p->second << "\n";  // 42 3.14
}

Forwarding for Getter/Setter

Perfect forwarding enables efficient setters that work with both lvalues and rvalues.

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

class Widget {
    std::string name_;

public:
    // Perfect forwarding setter
    void setName(auto&& name) {
        name_ = std::forward<decltype(name)>(name);
        // If called with lvalue: copies
        // If called with rvalue: moves
    }

    // Traditional approach: two overloads
    // void setName(const std::string& name) { name_ = name; }      // copy
    // void setName(std::string&& name) { name_ = std::move(name); } // move

    const std::string& getName() const { return name_; }
};

int main() {
    Widget w;

    std::string s = "hello";
    w.setName(s);              // Copies s
    std::cout << s << "\n";    // "hello" (still valid)

    w.setName(std::move(s));   // Moves from s
    std::cout << s << "\n";    // "" (moved-from)

    w.setName("temporary");    // Moves from temporary
}

Common Mistakes

Mistake 1: Using std::move instead of std::forward in forwarding functions

template <typename T>
void wrapper(T&& arg) {
    // Wrong: forces move even for lvalue inputs
    target(std::move(arg));

    // Correct: preserves value category
    target(std::forward<T>(arg));
}

Mistake 2: Forwarding the same argument twice

template <typename T>
void bad(T&& arg) {
    func1(std::forward<T>(arg));
    func2(std::forward<T>(arg));  // If arg was rvalue, it's been moved!
}

Mistake 3: Forgetting that named variables are lvalues

template <typename T>
void wrapper(T&& arg) {
    target(arg);  // Always lvalue! Need std::forward
}

Mistake 4: Applying std::forward to non-forwarding references

void func(int&& arg) {
    // arg is an rvalue reference, but it's a named lvalue
    target(std::forward<int>(arg));  // OK but unusual
    target(std::move(arg));          // More conventional for non-template
}

Mistake 5: Not using forwarding in variadic templates

template <typename... Args>
void bad(Args... args) {  // By value: always copies
    target(args...);
}

template <typename... Args>
void good(Args&&... args) {  // Forwarding references
    target(std::forward<Args>(args)...);
}

Practice Questions

  1. What does std::forward do? Answer: It conditionally casts its argument to an rvalue reference, restoring the original value category that the template parameter deduced.

  2. What is reference collapsing? Answer: The rule that determines the actual reference type when multiple references are nested. T& && collapses to T&, while T&& && collapses to T&&.

  3. Why is T&& a forwarding reference in templates but an rvalue reference in non-templates? Answer: In templates, T can be deduced as U&, triggering reference collapsing. In non-templates, T&& is always an rvalue reference.

  4. What is the output?

template <typename T>
void f(T&&) { std::cout << "f called\n"; }

int main() {
    int x = 5;
    f(x);
    f(5);
}

Answer: Both f calls compile. f(x) deduces T = int&, f(5) deduces T = int.

  1. Why should you not use std::move in return statements of forwarding functions? Answer: std::move prevents copy elision. std::forward is also unnecessary since return statements already handle value categories correctly.

FAQ

What is perfect forwarding in C++

Perfect forwarding passes function arguments through a wrapper to another function while preserving the original value category (lvalue/rvalue), const-ness, and reference-ness.

What is the difference between T&& in templates and T&& in non-templates

In templates (deduced context), T&& is a forwarding reference that can bind to both lvalues and rvalues. In non-templates, T&& is an rvalue reference that only binds to rvalues.

How does std::forward work internally

std::forward is essentially static_cast<T&&>(arg). When T is T&, this becomes static_cast<T&>(arg) (lvalue). When T is T, this becomes static_cast<T&&>(arg) (rvalue).

When should I use std::forward vs std::move

Use std::forward in template code for forwarding references (T&&). Use std::move in non-template code or when you know the object is an rvalue.

Can I forward the same argument multiple times

Only if it's an lvalue. For rvalues, forwarding moves the resources and the second forward will access a moved-from state.

Mini Project

Build a generic Delegate class that stores a callable and forwards arguments:

#include <iostream>
#include <string>
#include <functional>

// Your Delegate class with perfect forwarding

int main() {
    Delegate<void(int, double)> d;
    d.bind([](int a, double b) {
        std::cout << "Sum: " << a + b << "\n";
    });

    int x = 5;
    d.invoke(x, 3.14);  // Forward x as lvalue
    d.invoke(10, 2.71); // Forward as rvalues

    // Also works with move-only types
    Delegate<void(std::unique_ptr<int>)> moveOnly;
    moveOnly.bind([](std::unique_ptr<int> p) {
        std::cout << "Value: " << *p << "\n";
    });
    moveOnly.invoke(std::make_unique<int>(42));
}

This project mirrors how C++ standard library uses perfect forwarding in std::function, std::bind, and std::thread. Compare with Java which lacks value category distinction entirely.

What's Next

You now master perfect forwarding — the key to writing generic C++ wrappers. Next, you will learn structured bindings (C++17), which decompose tuples, pairs, arrays, and structs into named variables with clean syntax.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro