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?
std::move unconditionally casts to rvalue. std::forward conditionally forwards based on argument type
std::move is faster than std::forward
std::forward only works with templates
  1. In a template function, what is T&& called?
Universal reference (forwarding reference)
Rvalue reference
Lvalue reference
  1. Why do we need std::forward?
To preserve lvalue/rvalue nature through wrapper functions
To make code compile
To improve performance at runtime

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