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.
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 secondstd::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 thestd::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::move
s.