Reputation: 5412
Let's say I have a tuple t
whose type is std::tuple<T1,T2,T3>
where each T
may be either Obj
, Obj&
, Obj&&
,const Obj&
. I want to write a function which unpacks the values of the tuple into a function f
which accepts three types that decay to Obj
. I want to avoid useless copying as much as possible, how I do it?
My current implementation is
static R call(C2&& c, TUP&& x) {
using TUP0 = typename std::remove_reference<TUP>::type;
return c(std::forward<typename std::tuple_element<0, TUP0>::type>(std::get<0>(x)),
std::forward<typename std::tuple_element<1, TUP0>::type>(std::get<1>(x)),
std::forward<typename std::tuple_element<2, TUP0>::type>(std::get<2>(x)));
}
But this implementation seems to move
things when TUP
is std::tuple<Obj,Obj,Obj>
, even though it should only move when TUP contains an Obj&&
.
Upvotes: 1
Views: 2340
Reputation: 24946
If I get the question right, the desired behaviour is: Pass on an rvalue reference only if the element type is "rvalue reference" or if the element type is "value in an rvalue tuple" while otherwise passing on lvalue references.
T
= std::tuple<Obj, Obj&, Obj&&>&
object tuple
:Obj
) 1Obj&
)Obj&&
) 21: std::forward<std::tuple_element_t<I, T>>(std::get<I>(tuple)...)
won't do that job as the result will be Obj&&
. Also std::get<I>(std::move(tuple))...
won't either give the desired behaviour as the result will again be Obj&&
.
2: Here std::get<I>(std::forward<T>(tuple))...
won't give the desired value type as the result will be Obj&
.
T
= std::tuple<Obj, Obj&, Obj&&>&&
object tuple
:Obj
)Obj&
)Obj&&
)std::get<I>(std::forward<T>(tuple))...
will do the trick on rvalues.
Thus what I think you want is:
tuple<Obj>& -> Obj&
tuple<Obj&>& -> Obj&
tuple<Obj&&>& -> Obj&&
tuple<Obj>&& -> Obj&&
tuple<Obj&>&& -> Obj&
tuple<Obj&&>&& -> Obj&&
While the answer of @Oktalist will give you the desired behaviour, there's another way to do it:
namespace detail
{
template<std::size_t I, class ... Ts>
decltype(auto) get(std::tuple<Ts...>& x)
{
using T = std::tuple_element_t<I, std::tuple<Ts...>>;
return static_cast<std::conditional_t<
std::is_reference<T>::value, T, T&>>(std::get<I>(x));
}
template<std::size_t I, class ... Ts>
decltype(auto) get(std::tuple<Ts...>&& x)
{
return std::get<I>(std::move(x));
}
}
template<class C2, class TUP>
decltype(auto) call(C2&& c, TUP&& x) {
return std::forward<C2>(c)(
detail::get<0>(std::forward<TUP>(x)),
detail::get<1>(std::forward<TUP>(x)),
detail::get<2>(std::forward<TUP>(x)));
}
Upvotes: 3
Reputation: 14714
The problem is that std::forward
will cast a non-reference type to an rvalue reference. You want a function that will cast a non-reference type to an lvalue reference but preserve the reference category if the type is already a reference:
template <typename T>
constexpr decltype(auto) stable_forward(std::remove_reference_t<T>& arg) {
return static_cast<std::conditional_t<std::is_reference<T>::value, T, T&>>(arg);
}
template <typename T>
constexpr decltype(auto) stable_forward(std::remove_reference_t<T>&& arg) {
return static_cast<std::conditional_t<std::is_reference<T>::value, T, T&&>>(arg);
}
static R call(C2&& c, TUP&& x) {
using TUP0 = std::remove_reference_t<TUP>;
return c(stable_forward<std::tuple_element_t<0, TUP0>>(std::get<0>(std::forward<TUP>(x))),
stable_forward<std::tuple_element_t<1, TUP0>>(std::get<1>(std::forward<TUP>(x))),
stable_forward<std::tuple_element_t<2, TUP0>>(std::get<2>(std::forward<TUP>(x))));
}
Apologies for C++14, the transformation to C++11 is left as an exercise to the reader.
I've answered the question as asked. How wise it is I cannot say. Barry's answer might serve you better if you can adjust your code to supply the tuple as an rvalue.
Upvotes: 1
Reputation: 303137
In C++17, this is known as std::apply()
:
static R call(C2&& c, TUP&& x) {
return std::apply(std::forward<C2>(c), std::forward<TUP>(x));
}
This can be implemented in C++11 by using the index sequence trick. std::make_index_sequence
only got added to the standard library in C++14, but it itself is also implementable in C++11 and I will not include that implementation here:
namespace detail {
template <class F, class Tuple, size_t... Is>
auto apply_impl(F&& f, Tuple&& t, index_sequence<Is...>)
-> decltype(std::forward<F>(f)(std::get<Is>(std::forward<Tuple>(t))...))
{
return std::forward<F>(f)(std::get<Is>(std::forward<Tuple>(t))...);
}
}
template <class F, class Tuple>
auto apply(F&& f, Tuple&& t)
-> decltype(details::apply_impl(std::forward<F>(f), std::forward<Tuple>(t), make_index_sequence<std::tuple_size<typename std::decay<Tuple>::type>::value>{}))
{
return details::apply_impl(std::forward<F>(f), std::forward<Tuple>(t),
make_index_sequence<std::tuple_size<typename std::decay<Tuple>::type>::value>{});
}
There seems to be some confusion with what std::get()
actually does. Note that it depends on the reference qualification of the tuple. Here are the relevant overloads:
template< std::size_t I, class... Types >
constexpr std::tuple_element_t<I, tuple<Types...> >&
get( tuple<Types...>& t );
template< std::size_t I, class... Types >
constexpr std::tuple_element_t<I, tuple<Types...> >&&
get( tuple<Types...>&& t );
template< std::size_t I, class... Types >
constexpr std::tuple_element_t<I, tuple<Types...> >const&
get( const tuple<Types...>& t );
template< std::size_t I, class... Types >
constexpr std::tuple_element_t<I, tuple<Types...> >const&&
get( const tuple<Types...>&& t );
The returning type matches the const
and ref qualifications of the input tuple. That is given a std::tuple<int> a
, std::tuple<int&> b
, and std::tuple<int&&> c
:
std::get<0>(a); // int&
std::get<0>(std::move(a)); // int&&
std::get<0>(b); // int&
std::get<0>(std::move(b)); // int&, because reference collapsing
std::get<0>(c); // int&, because reference collapsing
std::get<0>(std::move(c)); // int&&
std::get<I>(std::forward<TUP>(x))
gives you the correct, safe type of reference, regardless of the type of the that member of the tuple. std::get<0>(c)
gives you an lvalue reference - which is correct behavior. If you want an rvalue reference out, you need an rvalue in. Per usual.
Upvotes: 5