Question

std::forward in C++: Perfect Forwarding, Purpose, and Problems Solved

cppc++11rvalue-referencec++-faqperfect-forwarding

Question

In a perfect-forwarding function, std::forward is used to convert the named rvalue-reference parameters t1 and t2 back to their original value categories before passing them to inner.

What is the purpose of doing this, and which problems does it solve? What effect would it have on overload resolution and the behavior of inner if t1 and t2 were passed directly as lvalues?

template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2)
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

Short Answer

std::forward preserves whether an argument was originally an lvalue or an rvalue. It is the key operation in perfect forwarding: a wrapper function can pass arguments to another function without accidentally turning every argument into an lvalue.

By the end, you will understand why named rvalue-reference variables are lvalues, how std::forward<T>(value) works, and when forwarding is appropriate.

Concept

A value's value category affects what a function can do with it:

  • An lvalue has an identity and can usually be used again, such as a named variable.
  • An rvalue is commonly a temporary value that may safely have resources moved from it.

C++ can overload functions based on these categories:

void inner(const std::string& text); // reads or copies
void inner(std::string&& text);      // may move from text

A parameter declared as T&& in a deduced template context is a forwarding reference (previously called a universal reference). It can bind to either category:

std::string name = "Ada";
outer(name);               // T is std::string&, parameter is std::string&
outer(std::string("Ada")); // T is std::string, parameter is std::string&&

However, there is an important rule: every named variable expression is an lvalue, even when its declared type is an rvalue reference.

void example(std::string&& value)
{
    // `value` has type std::string&&,
    // but the expression `value` is an lvalue.
}

Therefore, calling inner(t1) directly always passes an lvalue expression. std::forward<T1>(t1) conditionally casts t1:

  • If the original caller supplied an lvalue, it remains an lvalue.
  • If the original caller supplied an rvalue, it becomes an rvalue again.

This lets wrapper functions, factory functions, containers, and adapters preserve the caller's intent. In particular, it allows an eventual target function to choose move-aware overloads for temporary objects while continuing to treat reusable named objects as lvalues.

Mental Model

Think of outer as a delivery service receiving packages.

  • An lvalue is a package the sender still owns and may want back. The delivery service should handle it carefully and not treat it as disposable.
  • An rvalue is a temporary package the sender has finished with. The receiver may unpack it and reuse its contents.

When the package arrives at outer, it gets a label: t1. Because it now has a name, C++ treats it as a reusable package—an lvalue—even if it was originally temporary.

std::forward<T1>(t1) checks the original delivery instructions stored in T1 and restores them for inner: “this was borrowed” or “this may be consumed.”

Take Quiz

Syntax and Examples

The normal perfect-forwarding pattern is:

template <typename T>
void outer(T&& value)
{
    inner(std::forward<T>(value));
}

Use the same template type parameter in both places:

std::forward<T>(value)

Here is a complete example with overloads:

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

void inner(const std::string& text)
{
    std::cout << "inner received an lvalue: " << text << '\n';
}

void inner(std::string&& text)
{
    std::cout << "inner received an rvalue: " << text << '\n';
}

template <typename T>
void outer(T&& text)
{
    inner(std::forward<T>(text));
}

int main()
{
    std::string name = "Ada";

    outer(name);                 // calls inner(const std::string&)
    outer(std::string("Grace")); // calls inner(std::string&&)
}

std::forward does not inherently move an object or destroy it. It preserves the category encoded in T, allowing the receiving function to decide whether it should copy, read, or move from the argument.

Include <utility> to use std::forward.

Step by Step Execution

Consider this program:

#include <string>
#include <utility>

void inner(const std::string& value);
void inner(std::string&& value);

template <typename T>
void outer(T&& value)
{
    inner(std::forward<T>(value));
}

int main()
{
    std::string title = "Report";
    outer(title);
    outer(std::string("Draft"));
}

First call: outer(title)

  1. title is a named variable, so it is an lvalue.
  2. Template deduction makes T equal to std::string&.
  3. Reference collapsing makes T&& become std::string&; value refers to title.
  4. std::forward<std::string&>(value) produces an lvalue.
  5. The lvalue overload, inner(const std::string&), is selected.

Second call: outer(std::string("Draft"))

  1. std::string("Draft") is a temporary, so it is an rvalue.
  2. Template deduction makes T equal to std::string.
  3. The parameter type is std::string&&.
  4. Although value is a named lvalue expression inside outer, std::forward<std::string>(value) produces an rvalue.
  5. The rvalue overload, inner(std::string&&), is selected.

Without std::forward, both calls use inner(value). Since value is named, both are treated as lvalues and the rvalue overload is not selected.

Real World Use Cases

std::forward is useful when a function receives arguments only to pass them onward.

  • Factory functions: Construct an object directly from arbitrary constructor arguments.

    template <typename T, typename... Args>
    T make_object(Args&&... args)
    {
        return T(std::forward<Args>(args)...);
    }
    
  • Container insertion: Forward a value to an element constructor so temporaries can be moved instead of copied.

    template <typename T>
    void add_item(T&& item)
    {
        items.emplace_back(std::forward<T>(item));
    }
    
  • Callback wrappers: Pass an event or request to a callback while preserving whether it is temporary or reusable.

  • Logging and formatting wrappers: Relay formatting arguments to another API without creating unnecessary copies.

  • Dependency injection and builders: Forward constructor parameters through one or more wrapper layers to the final object.

Take Quiz

Real Codebase Usage

In production C++ code, perfect forwarding is most common in generic utilities and constructors.

Forwarding a parameter pack

Each argument needs its corresponding template type:

template <typename Function, typename... Args>
decltype(auto) invoke_wrapper(Function&& function, Args&&... args)
{
    return std::forward<Function>(function)(std::forward<Args>(args)...);
}

This allows the wrapper to preserve both the callable's category and every argument's category.

Forwarding constructors

A class may forward arguments to a member's constructor:

#include <string>
#include <utility>

class Message
{
public:
    template <typename T>
    explicit Message(T&& text)
        : text_(std::forward<T>(text))
    {
    }

private:
    std::string text_;
};

Passing a std::string lvalue copies its content into text_; passing a temporary std::string lets text_ use its move constructor.

Important design guideline

Use forwarding references when your function is a generic relay. If a function needs to store, inspect, or use a value more than once, taking a value by value or by const& is often clearer and safer. Perfect forwarding can make overload resolution and error messages more complex, so it should solve a real forwarding need.

Common Mistakes

Forgetting std::forward

This compiles, but it loses rvalue-ness:

template <typename T>
void outer(T&& value)
{
    inner(value); // Always passes an lvalue expression.
}

Use std::forward<T>(value) when relaying value to another function.

Using std::move instead of std::forward

template <typename T>
void outer(T&& value)
{
    inner(std::move(value)); // Wrong for a general forwarding wrapper.
}

std::move unconditionally produces an rvalue. If the caller passed an lvalue, this may allow inner to move from the caller's object unexpectedly. std::forward<T> is conditional and preserves the original category.

Forwarding with the wrong type

template <typename T>
void outer(T&& value)
{
    inner(std::forward<std::string>(value)); // Usually wrong.
}

Use the deduced type parameter associated with that parameter:

inner(std::forward<T>(value));

Forwarding the same rvalue more than once

template <typename T>
void duplicate(T&& value)
{
    first(std::forward<T>(value));
    second(std::forward<T>(value)); // May observe a moved-from object.
}

If the original argument was an rvalue, first may move from it. Do not assume it still contains its original value afterward.

Treating T&& as always an rvalue reference

T&& is a forwarding reference only when T is deduced in the relevant context. For example, std::vector<T>&& is simply an rvalue reference; it does not accept lvalue vectors through the same deduction behavior.

Forwarding after using an object as if it were unchanged

A function can inspect an argument before forwarding it, but if it modifies it or relies on a stable value afterward, forwarding an rvalue may be inappropriate. Clearly define whether the function may consume its argument.

Comparisons

Expression or techniqueResultBest use
inner(value)value is passed as an lvalue because it is namedThe target should always borrow/read the object
inner(std::move(value))Forces value to be treated as an rvalueYou deliberately want to allow moving, regardless of how it was received
inner(std::forward<T>(value))Preserves the original lvalue/rvalue categoryGeneric forwarding wrappers
const T& parameterBinds to lvalues and rvalues but does not permit moving from the sourceRead-only input
T parameterMakes a local copy or move of the argumentThe function needs an owned local value

std::move versus std::forward

Both are casts; neither moves data by itself. A move happens only if a selected operation, such as a move constructor, uses the resulting rvalue.

  • std::move(x) means: “Treat x as an rvalue now.”
  • std::forward<T>(x) means: “Treat x as the category originally represented by T.”

For a forwarding-reference parameter, use std::forward<T>(x). For an object your function owns and no longer needs, use std::move(x).

Take Quiz

Cheat Sheet

#include <utility>

template <typename T>
void wrapper(T&& value)
{
    target(std::forward<T>(value));
}
  • T&& is a forwarding reference when T is deduced.
  • A named parameter is always an lvalue expression, including a parameter declared as Type&&.
  • std::forward<T>(value) preserves the caller's original category.
  • Use std::forward when passing a forwarding-reference parameter onward.
  • Use the matching type: std::forward<T>(value).
  • Use std::move when you intentionally want to treat an object as expendable.
  • Forwarding an rvalue may let another function move from it.
  • Do not forward the same potentially consumed rvalue repeatedly unless moved-from behavior is acceptable.

Reference collapsing rules:

Written combinationFinal type
T& &T&
T& &&T&
T&& &T&
T&& &&T&&

FAQ

Why is a named rvalue reference an lvalue in C++?

A name identifies an object that can be used again within its scope. Therefore, an expression such as value is an lvalue even if value was declared as Type&&.

Does std::forward move an object?

No. It casts based on T. A later constructor, assignment operator, or function overload may move from the object if it receives an rvalue.

What happens if I omit std::forward?

The target function sees named parameters as lvalues. Rvalue overloads may not be selected, and temporary objects may be copied instead of moved.

When should I use std::move instead of std::forward?

Use std::move when your function owns an object and intentionally gives up its value. Use std::forward<T> when relaying a deduced T&& parameter while preserving the caller's category.

Can I use std::forward with an ordinary parameter?

You can, but it is mainly intended for forwarding-reference patterns. With an ordinary parameter, there is often no original category to preserve, and std::move or no cast may better communicate your intent.

Is const T&& a forwarding reference?

No. A forwarding reference has the form T&& where T is deduced. Adding const prevents the special lvalue-binding behavior.

Why is std::forward<T>(value) written with T explicitly?

The template argument tells std::forward whether value should become an lvalue or rvalue. It uses the type deduced when outer was called.

Take Quiz

Mini Project

Description

Build a small forwarding wrapper that sends text to overloaded handlers. The project demonstrates how omitting std::forward causes a temporary string to be treated as an lvalue, while correct forwarding preserves move-aware behavior.

Goal

Create a relay function that calls the correct handle overload for both named strings and temporary strings.

Requirements

  1. Create one handle overload for const std::string& and one for std::string&&.
  2. Implement a templated relay function that accepts a forwarding reference.
  3. Forward the argument from relay to handle while preserving its value category.
  4. Call relay once with a named std::string and once with a temporary string.
  5. Print which overload handled each argument.
Take Quiz

Keep learning

Related questions

View more