lost rvalue references if forward_as_tuple result is stored in variable before using it in std::apply

Viewed 234

While working on a project I encounter a situation std::apply does not forward rvalue references from std::tuple created by std::forward_as_tuple *IF* resulting std::tuple is stored in a variable! However if std::forward_as_tuple result is not stored in a variable, but is just passed as a second argument to std::apply then it works and rvalue reference gets perfectly forwarded.

I tried many options including using different types for std::tuple, like

decltype(auto) t = forward_as_tuple(1, std::move(r))
auto t = forward_as_tuple(1, std::move(r))
auto&& t = forward_as_tuple(1, std::move(r))
auto& t = forward_as_tuple(1, std::move(r))

Nothing helped to store a tuple in a variable and then pass it to std::apply. It appears like lvalue reference being forwarded at the end to std::__invoke by std::apply... There is a godbolt link to my code: https://godbolt.org/z/24LYP5

Code snippet

#include <functional>
#include <iostream>

auto product(int l, int&& r) {  return l * r; }

static void test_not_works()
{
    int r = 2;
    decltype(auto) t = std::forward_as_tuple(1, std::move(r));
    std::apply(product, t);    
}

static void test_works()
{
    int r = 2;    
    std::apply(product, std::forward_as_tuple(1, std::move(r)));    
}

Error message

In file included from /opt/compiler-explorer/gcc-7.3.0/include/c++/7.3.0/functional:54:0,

                 from <source>:1:

/opt/compiler-explorer/gcc-7.3.0/include/c++/7.3.0/tuple: In instantiation of 'constexpr decltype(auto) std::__apply_impl(_Fn&&, _Tuple&&, std::index_sequence<_Idx ...>) [with _Fn = int (&)(int, int&&); _Tuple = std::tuple<int&&, int&&>&; long unsigned int ..._Idx = {0, 1}; std::index_sequence<_Idx ...> = std::integer_sequence<long unsigned int, 0, 1>]':

/opt/compiler-explorer/gcc-7.3.0/include/c++/7.3.0/tuple:1671:31:   required from 'constexpr decltype(auto) std::apply(_Fn&&, _Tuple&&) [with _Fn = int (&)(int, int&&); _Tuple = std::tuple<int&&, int&&>&]'

<source>:10:26:   required from here

/opt/compiler-explorer/gcc-7.3.0/include/c++/7.3.0/tuple:1662:27: error: no matching function for call to '__invoke(int (&)(int, int&&), int&, int&)'

       return std::__invoke(std::forward<_Fn>(__f),

              ~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~

       std::get<_Idx>(std::forward<_Tuple>(__t))...);

       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

In file included from /opt/compiler-explorer/gcc-7.3.0/include/c++/7.3.0/tuple:41:0,

                 from /opt/compiler-explorer/gcc-7.3.0/include/c++/7.3.0/functional:54,

                 from <source>:1:

/opt/compiler-explorer/gcc-7.3.0/include/c++/7.3.0/bits/invoke.h:89:5: note: candidate: template<class _Callable, class ... _Args> constexpr typename std::__invoke_result<_Functor, _ArgTypes>::type std::__invoke(_Callable&&, _Args&& ...)

     __invoke(_Callable&& __fn, _Args&&... __args)

     ^~~~~~~~

/opt/compiler-explorer/gcc-7.3.0/include/c++/7.3.0/bits/invoke.h:89:5: note:   template argument deduction/substitution failed:

/opt/compiler-explorer/gcc-7.3.0/include/c++/7.3.0/bits/invoke.h: In substitution of 'template<class _Callable, class ... _Args> constexpr typename std::__invoke_result<_Functor, _ArgTypes>::type std::__invoke(_Callable&&, _Args&& ...) [with _Callable = int (&)(int, int&&); _Args = {int&, int&}]':

/opt/compiler-explorer/gcc-7.3.0/include/c++/7.3.0/tuple:1662:27:   required from 'constexpr decltype(auto) std::__apply_impl(_Fn&&, _Tuple&&, std::index_sequence<_Idx ...>) [with _Fn = int (&)(int, int&&); _Tuple = std::tuple<int&&, int&&>&; long unsigned int ..._Idx = {0, 1}; std::index_sequence<_Idx ...> = std::integer_sequence<long unsigned int, 0, 1>]'

/opt/compiler-explorer/gcc-7.3.0/include/c++/7.3.0/tuple:1671:31:   required from 'constexpr decltype(auto) std::apply(_Fn&&, _Tuple&&) [with _Fn = int (&)(int, int&&); _Tuple = std::tuple<int&&, int&&>&]'

<source>:10:26:   required from here

/opt/compiler-explorer/gcc-7.3.0/include/c++/7.3.0/bits/invoke.h:89:5: error: no type named 'type' in 'struct std::__invoke_result<int (&)(int, int&&), int&, int&>'

Compiler returned: 1
2 Answers

When rvalue references are passed to std::forward_as_tuple, it constructs a std::tuple of rvalue references. So first of all, declaring t as auto t = std::forward_as_tuple(...) is fine, the "rvalueness" of the object is encoded inside the type generated by std::forward_as_tuple.

But then note that there is something special about the first argument to std::apply: product takes an int parameter by value, and a second one as int&&, i.e., an rvalue-reference. Calling such a function obviously requires the second argument to be an rvalue reference, but this does only work if you make sure it is one. Hence:

auto t = std::forward_as_tuple(1, std::move(r));
std::apply(product, std::move(t));
                   // ^^^^^^^ cast to rvalue here

Another possible fix is to change product to accept to plain ints by value (in case of an int, there is no performance hit anyway). Then, you can pass t as an lvalue, too.

std::apply perfect-forwards the tuple to std::get to access elements of the tuple. std::get returns an lvalue reference if its tuple argument is an lvalue. Since your t variable is an lvalue, std::apply calls product with an int& instead of an int&&.

I can't speak for the motivation behind having it work this way despite the tuple explicitly holding an rvalue reference. However, a simple way to get the same behaviour as a temporary here is to use std::move to produce an xvalue, which will handle stored references in the way you want:

std::apply(product, std::move(t));  

Note, however, that unlike using just t, any non-reference types in the tuple should now be treated as moved from. I'm unsure if there's a simple way to both use the tuple's reference types and treat non-reference types as lvalues for an lvalue tuple. Granted this won't be an issue when creating the tuple using forward_as_tuple since it will always contain references.

Related