Advanced 14 min

Perfect Forwarding

Master perfect forwarding with std::forward for writing efficient generic code

Learn perfect forwarding - the technique that enables writing generic wrapper functions that preserve argument types and enable optimal performance.

A Simple Example

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

class Widget {
public:
    Widget() { std::cout << "Widget default constructed\n"; }
    Widget(const Widget&) { std::cout << "Widget copied\n"; }
    Widget(Widget&&) noexcept { std::cout << "Widget moved\n"; }
};

template<typename T, typename... Args>
std::unique_ptr<T> make_unique_verbose(Args&&... args) {
    std::cout << "Creating object...\n";
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

int main() {
    Widget w;

    auto p1{make_unique_verbose<Widget>()};       // Default construct
    auto p2{make_unique_verbose<Widget>(w)};      // Copy
    auto p3{make_unique_verbose<Widget>(Widget{})};  // Move

    return 0;
}

Breaking It Down

Universal References T&&

  • In template context, T&& is a universal reference (also called forwarding reference)
  • Can bind to both lvalues and rvalues, unlike regular rvalue references
  • Type deduction: lvalue argument gives T&, rvalue argument gives T&&
  • Remember: Only T&& in template context is universal. Widget&& is always an rvalue reference

std::forward<T>() - Perfect Forwarding

  • What it does: Forwards arguments preserving their value category (lvalue or rvalue)
  • Usage: std::forward<T>(arg) inside template functions
  • Purpose: Enables move semantics to work through wrapper functions
  • Remember: Use std::forward with universal references, std::move with rvalue references

How Perfect Forwarding Works

  • Step 1: Universal reference Args&&... captures argument types
  • Step 2: std::forward preserves lvalue/rvalue nature
  • Step 3: Inner function receives arguments as if called directly
  • Remember: Without std::forward, all arguments become lvalues inside the function

Reference Collapsing Rules

  • T& & collapses to T& (lvalue reference)
  • T& && collapses to T& (lvalue reference)
  • T&& & collapses to T& (lvalue reference)
  • T&& && collapses to T&& (rvalue reference)
  • Remember: An lvalue anywhere means lvalue; only rvalue + rvalue gives rvalue

Why This Matters

  • When writing factory functions or wrappers, you want to pass arguments efficiently without extra copies.
  • Perfect forwarding preserves whether an argument is an lvalue or rvalue, enabling move semantics to work correctly.
  • This is the foundation of std::make_unique, std::make_shared, and many other standard library utilities.
  • Understanding perfect forwarding unlocks the full power of variadic templates and generic programming.

Critical Insight

Perfect forwarding is like being a perfect messenger. Imagine you are delivering packages - some people hand you their package directly (rvalue), others show you where their package is stored (lvalue). A perfect messenger remembers which is which and delivers accordingly.

Without std::forward, all packages would be treated as if they need to be copied. With std::forward, temporary packages can be moved efficiently, while referenced packages are passed by reference. The inner function receives arguments exactly as if called directly - no performance lost in translation!

Best Practices

Always use std::forward with universal references: When you have T&& in a template, use std::forward<T>() to forward it.

Use std::forward only once per argument: Forwarding consumes the argument. Do not forward the same argument multiple times.

Combine with variadic templates: Perfect forwarding shines with parameter packs Args&&... args and std::forward<Args>(args)....

Understand when to use std::move vs std::forward: Use std::move when you know you have an rvalue, std::forward for universal references.

Common Mistakes

Forgetting std::forward: Without it, all arguments become lvalues and move semantics breaks.

Using std::move instead of std::forward: std::move always gives rvalue, breaking lvalue forwarding.

Forwarding the same argument twice: After forwarding (or moving), do not use the argument again.

Mixing up std::forward with regular rvalue references: std::forward only works correctly with universal references (T&& in templates).

Debug Challenge

This wrapper function forces copies instead of preserving move semantics. Click the highlighted line to fix it:

1 template<typename T, typename Arg>
2 std::unique_ptr<T> makeUnique(Arg&& arg) {
3 return std::unique_ptr<T>(new T(arg));
4 }

Quick Quiz

  1. What is the difference between std::move and std::forward?
They are interchangeable
std::move unconditionally casts to rvalue. std::forward conditionally forwards based on argument type
std::forward only works with templates
std::move is faster than std::forward
  1. In a template function, what is T&& called?
Const reference
Lvalue reference
Universal reference (forwarding reference)
Rvalue reference
  1. Why do we need std::forward?
To make code compile
To improve performance at runtime
To avoid copying large objects
To preserve lvalue/rvalue nature through wrapper functions

Step Through the Code

Walk through the code step by step. Watch how variables change and see the program output at each line.

Lesson Progress

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