Search code examples
c++templatesreferencervaluestdmove

How does std::forward work in the context of a fold expression?


In the MSVC STL, the implementation of std::apply is as follows:

template <class _Callable, _Tuple_like _Tuple, size_t... _Indices>
constexpr decltype(auto) _Apply_impl(_Callable&& _Obj, _Tuple&& _Tpl, index_sequence<_Indices...>) noexcept(/* [...] */) {
    return _STD invoke(_STD forward<_Callable>(_Obj), _STD get<_Indices>(_STD forward<_Tuple>(_Tpl))...);
}

In the expression _STD get<_Indices>(_STD forward<_Tuple>(_Tpl))..., std::get is called the same number of times as the length of the tuple. However, with each invocation of std::get, there is a corresponding invocation of std::forward. When forwarding an rvalue, std::forward is equivalent to std::move, implying multiple calls to std::move for the _Tpl object. This seems to be potentially invalid. Could someone help explain my concern?

I've tried searching through various resources, but I couldn't find the answer I was looking for. Starting from the definition also hasn't convinced me.


Solution

  • implying multiple calls to std::move for the _Tpl object. This seems to be potentially invalid.

    Yes, there are multiple calls to std::move on the tuple. But this in itself is not invalid.

    Generally, passing an object with std::move to a function is a promise to that function that the caller won't rely on the state of the object after the call. Therefore a second std::move to another function would generally have incorrect semantics, because it would pass an unspecified state to the second function.

    But there are several cases where a function makes stronger promises to the caller. For example std::unique_ptr promises that after calling its move constructor or move assignment the passed object will be in an empty state. So it can still be used as a null pointer after the std::move.

    For std::get is known even more precisely what it will do: It will do nothing except return a reference to the requested element of the tuple. And depending on whether the tuple was passed as rvalue or lvalue it will also return lvalue or rvalue references as appropriate when combined with the element's type.

    So, std::get won't actually modify the state of the tuple and the pack expansion (which is not a fold expression btw.) will result in a list of one reference to each element of tuple, potentially as lvalue or rvalue, but never two rvalues to the same object. Thus, when passing the references on to the invoked function, there is no problem equivalent to generic double-std::moves.