Forwarding references and std::forward

A wrapper should hand its argument to the target unchanged: an lvalue stays an lvalue, a temporary stays a temporary, and const stays const. T&& in a deduced context, called a forwarding reference, plus std::forward does exactly that.

The mechanism is two rules: reference collapsing, which turns T&& into T& when T is itself a reference, and std::forward, which restores the original category at the call. Lesson 032 introduced rvalue references; this lesson uses them to pass something through.

What T&& deduces

In template <typename T> void probe(T&&), T is deduced from the call. An lvalue argument of type int gives T = int&, and T&& then collapses to int&; an rvalue gives T = int and T&& stays int&&.

The program prints the two questions the traits can answer: is T an lvalue reference, and is T&& an rvalue reference. The first call prints 1 0, the second 0 1.

The collapse rule is & && becomes &, && && becomes &&. It applies to every reference to a reference in a template, and it is why a variable declared T&& is not always an rvalue reference.

The same deduction works with any number of arguments, which is what makes a variadic factory possible: each argument keeps its own category through the pack.

#include <iostream> #include <type_traits> template <typename T> void probe(T&&) { std::cout << std::is_lvalue_reference_v<T> << ' ' << std::is_rvalue_reference_v<T&&> << '\n'; } int main() { int x = 1; probe(x); probe(1); }

g++ -std=c++23 -Wall -Wextra collapse.cpp -o collapse
./collapse

1 0
0 1

std::forward at the call

Inside a forwarding-reference function the parameter has a name, and a named variable is an lvalue. Passing it on without a cast would always choose the const-reference overload, even when the caller gave a temporary.

std::forward<T>(value) casts back to the original category: an lvalue for an lvalue argument, an rvalue for an rvalue. The program's relay forwards a named string, a temporary and a literal, and the overload set prints copy for the first and move for the other two.

The literal reaches the rvalue overload because the conversion to std::string produces a temporary. That is the behaviour a wrapper should preserve, and it is what std::make_unique does with its arguments.

#include <iostream> #include <string> #include <utility> void sink(const std::string& text) { std::cout << "copy " << text << '\n'; } void sink(std::string&& text) { std::cout << "move " << text << '\n'; } template <typename T> void relay(T&& value) { sink(std::forward<T>(value)); } int main() { std::string name = "Ada"; relay(name); relay(std::string{"Bob"}); relay("literal"); }

g++ -std=c++23 -Wall -Wextra forward.cpp -o forward_demo
./forward_demo

copy Ada
move Bob
move literal

A factory that loses nothing

A variadic forwarding function builds an object from whatever arguments it is given. The pack is forwarded element by element, so each constructor parameter receives the same category the caller used.

The widget below has a const-reference and an rvalue-reference constructor, and the program builds one from a named string and one from a temporary. The printed lines show which constructor ran for each call.

This is the shape of std::make_unique and std::make_shared, and of every emplace function in the standard library. part 8 uses it when the smart pointers arrive.

#include <iostream> #include <string> #include <utility> class widget { public: explicit widget(const std::string& name) : name_(name) { std::cout << "copy " << name_ << '\n'; } explicit widget(std::string&& name) : name_(std::move(name)) { std::cout << "move " << name_ << '\n'; } private: std::string name_; }; template <typename T, typename... Args> T build(Args&&... args) { return T(std::forward<Args>(args)...); } int main() { std::string name = "panel"; widget a = build<widget>(name); widget b = build<widget>(std::string{"button"}); (void)a; (void)b; }

g++ -std=c++23 -Wall -Wextra factory.cpp -o factory
./factory

copy panel
move button

forward keeps the category

The difference between std::move and std::forward is visible when the same function is called with an lvalue and with a temporary. std::move always produces an rvalue; std::forward produces the category the caller used.

The program takes a string through a forwarding reference and copies it with auto. For the named string the copy is a copy and the original keeps its size; for the temporary the copy is a move and the source is left empty.

That empty line is the moved-from state of lesson 032. It is not a bug here: the temporary was never going to be used again.

#include <iostream> #include <string> #include <utility> template <typename T> void consume(T&& value) { auto copy = std::forward<T>(value); std::cout << copy.size() << ' ' << value.size() << '\n'; } int main() { std::string name = "named"; consume(name); consume(std::string{"temporary"}); }

g++ -std=c++23 -Wall -Wextra category.cpp -o category
./category

5 5
9 0

The const comes along

A forwarding reference deduces const as part of T. A const int lvalue gives T = const int&, and the parameter is a reference to const, so a write inside the function is rejected.

The program bumps its argument and is called with a const int. The error says the assignment writes through a read-only reference and names the instantiation with T = const int&, which is the useful part of the message.

The fix is to take the parameter by value when the function modifies it, or to give the caller a non-const object. A forwarding reference is for passing something on, not for changing it in place.

template <typename T> void bump(T&& value) { value += 1; } int main() { const int x = 1; bump(x); }

g++ -std=c++23 -Wall -Wextra const_forward.cpp -o const_forward

const_forward.cpp: In instantiation of 'void bump(T&&) [with T = const int&]':
const_forward.cpp:10:9: required from here
10 | bump(x);
| ~~~~^~~
const_forward.cpp:4:11: error: assignment of read-only reference 'value'
4 | value += 1;
| ~~~~~~^~~~

Exercises

  1. Write a function that prints whether its argument arrived as an lvalue or an rvalue, and call it with both.
  2. Add a third sink overload that takes an int and check that relay(42) reaches it.
  3. Replace std::forward with std::move in forward.cpp and compare the output of the three calls.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does T&& deduce when the argument is an lvalue of type Widget?

    Answer

    T is Widget& and the parameter is Widget&

    A forwarding reference deduces the reference into T, and reference collapsing turns T&& into Widget&.

  2. What is the result of reference collapsing T& && when T is deduced as Widget&?

    Answer

    Widget&

    An lvalue reference combined with an rvalue reference collapses to an lvalue reference.

  3. What does std::forward<T>(value) do?

    Answer

    It casts to T&&, keeping the original value category

    The explicit T is what preserves the category; without it the cast would be a plain move.

  4. What happens to a const lvalue passed through a forwarding reference?

    Answer

    It arrives as a const lvalue and cannot be modified

    The const is part of the deduced type, and forward keeps it.

  5. What does this program print? #include <iostream> #include <type_traits> template <typename T> void check(T&&) { std::cout << std::is_lvalue_reference_v<T> << ' ' << std::is_rvalue_reference_v<T&&>; } int main() { int x = 1; check(x); std::cout << '\n'; check(2); }

    Answer

    1 0 / 0 1

    The lvalue deduces int&, which is an lvalue reference, and the int literal deduces int, which is neither reference.

  6. What does this program print? #include <iostream> #include <string> #include <utility> void take(const std::string& s) { std::cout << "copy " << s << '\n'; } void take(std::string&& s) { std::cout << "move " << s << '\n'; } template <typename T> void relay(T&& value) { take(std::forward<T>(value)); } int main() { std::string name = "Ada"; relay(name); relay(std::string{"Bob"}); relay("literal"); }

    Answer

    copy Ada / move Bob / move literal

    The lvalue stays an lvalue, the temporary stays an rvalue, and the literal creates a temporary string.

  7. What does this program print? #include <iostream> #include <memory> #include <utility> struct widget { std::string name; }; struct panel { std::string name; }; template <typename T, typename U> std::unique_ptr<T> make(U&& value) { return std::make_unique<T>(T{std::forward<U>(value)}); } int main() { panel source{"panel"}; auto first = make<panel>(source); auto second = make<widget>(std::string{"button"}); std::cout << first->name << ' ' << second->name; }

    Answer

    panel button

    The lvalue is copied into the new object, the temporary is moved, and both names survive.

  8. What does this program print? #include <iostream> #include <utility> template <typename T> T&& keep(T&& value) { return std::forward<T>(value); } int main() { int x = 5; int& a = keep(x); std::cout << &a - &x << ' ' << a << '\n'; int b = keep(9); std::cout << b << ' ' << 0; }

    Answer

    0 5 / 9 0

    The forwarded lvalue is the same object, so the address difference is zero, and the value 9 is copied from the temporary.

  9. What does the compiler say about this code? #include <iostream> template <typename T> void bump(T&& value) { value += 1; } int main() { const int x = 1; bump(x); std::cout << x; }

    Answer

    assignment of read-only reference

    The const int& deduced into T keeps the const, and the assignment to it is rejected.

  10. What breaks when a wrapper calls std::move on its forwarding reference?

    Answer

    Every lvalue argument is treated as an rvalue and may be moved from

    The unconditional move steals from a caller's lvalue, which is why only std::forward preserves the category.

The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.

Article author: Arthur Isaev