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

1

5

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.

Encompass answered 15/12, 2023 at 7:22 Comment(2)
Please note that there are many implementations of the C++ standard library. There's no single "the" standard library.Avram
As for std::move, it doesn't really do anything, all it does is converting the argument to an rvalue reference. Why would it be problematic with many move calls? Why would it be invalid?Avram
B
6

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.

Behah answered 15/12, 2023 at 7:41 Comment(4)
One might naively try to forward elements rather than the tuple itself; but that would almost always move the elements and create UB for most cases. By forwarding the top level object(tuple in this case), code allows the compiler to choose correct behavior for elements.Syllabic
@Behah std::forward_like has a different intended use case. I remember it from the deduced this approved C++20 feature proposal. forward_like seems to have emerged as a follow up proposal. For apply-tuple case, imagine what happens for reference capturing tuples(std::tie, std:: forward_as_tuple); top-level forward keeps the template sane, while element-wise forwarding(forward_like) changes valueness and results in ill-formed program.Syllabic
@Syllabic Ehm yes, I somehow wasn't thinking straight there.Behah
@Behah No! You were. I just happened to have once tried to implement tuple. It leads to emotional damage;)Syllabic

© 2022 - 2024 — McMap. All rights reserved.