How std::forward_as_tuple Preserves Value Categories
In C++, std::forward_as_tuple allows developers to
bundle heterogeneous arguments into a single std::tuple
while strictly preserving the original value categories—both lvalues and
rvalues—of each element. By taking arguments via forwarding references
and applying C++ reference collapsing rules, the function constructs a
tuple composed entirely of reference types (T& or
T&&) without making copies or premature moves. This
article explains the underlying language mechanics behind
std::forward_as_tuple, detailing how reference deduction,
reference collapsing, and tuple instantiation collaborate to enable
perfect forwarding for bundled function arguments.
The Problem of Bundled Forwarding
When forwarding individual arguments to a function,
std::forward<T>(arg) suffices. However, certain
interfaces require multiple arguments to be packaged together before
being forwarded to another construct. A primary example is
std::pair piecewise construction using
std::piecewise_construct, where the constructors of two
separate objects must receive their arguments as tuples.
Using std::make_tuple in these scenarios fails to
preserve value categories because it decays types, stripping references
and making copies or moving objects into the tuple elements. Using
std::tie also fails because it can only bind to lvalue
references (T&), rejecting rvalues or temporaries
unless bound to const&, which prevents moving.
std::forward_as_tuple solves this gap.
The Function Signature and Universal References
The standard defines std::forward_as_tuple essentially
as follows:
template <typename... Args>
constexpr std::tuple<Args&&...> forward_as_tuple(Args&&... args) noexcept;The function takes a parameter pack Args&&....
Because Args is a template parameter being deduced in a
function template, Args&& constitutes a forwarding
reference (or universal reference).
When an argument is passed to std::forward_as_tuple,
template argument deduction determines Args based on the
argument's value category:
- If the argument is an lvalue of type
T,Argsis deduced as an lvalue reference:T&. - If the argument is an rvalue (a temporary or the
result of
std::move) of typeT,Argsis deduced as the object type:T.
Reference Collapsing Rules
C++ applies reference collapsing when references to references arise during template instantiation. The rules specify:
&with&becomes&&with&&becomes&&&with&becomes&&&with&&becomes&&
Applying these rules to the return type
std::tuple<Args&&...> demonstrates how value
categories are maintained:
- For an lvalue argument of type
T:Argsis deduced asT&.- The parameter
Args&&becomesT& &&, which collapses toT&. - The return type element
Args&&also becomesT& &&, which collapses toT&. - The resulting tuple stores an lvalue reference
(
T&), preserving lvalue semantics.
- For an rvalue argument of type
T:Argsis deduced asT.- The parameter
Args&&becomesT&&. - The return type element
Args&&becomesT&&. - The resulting tuple stores an rvalue reference
(
T&&), preserving rvalue semantics and permitting future moves.
Forwarding the Tuple Elements
Inside the implementation of std::forward_as_tuple, the
arguments are forwarded to the tuple constructor:
return std::tuple<Args&&...>(std::forward<Args>(args)...);Because std::tuple has a constructor capable of holding
references, the tuple itself does not store instances of T.
Instead, it stores references directly bound to the actual arguments
passed by the caller.
When the receiving function unpacks the tuple (for example, via
std::apply or piecewise constructors), accessing a
T&& element yields an xvalue (an rvalue), while
accessing a T& element yields an lvalue. This ensures
that the destination constructor or function receives the exact value
category initially supplied to std::forward_as_tuple.
Lifetime Considerations
Because std::forward_as_tuple stores references rather
than owning values, it introduces lifetime constraints. Rvalue
references stored in the tuple do not extend the lifetime of temporary
objects beyond the full-expression containing the call. Consequently,
std::forward_as_tuple is designed strictly for immediate
forwarding within the same expression and should not be stored or
returned across broader scopes, as doing so leads to dangling
references.