tohava
tohava

Reputation: 5412

C++: Unpack a tuple with references and values without copying/moving too much

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

Answers (3)

Pixelchemist
Pixelchemist

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.

1. In case of an lvalue T = std::tuple<Obj, Obj&, Obj&&>& object tuple:

  • Don't move first element (Obj) 1
  • Don't move second element (Obj&)
  • Move third element (Obj&&) 2

1: 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&.

2. In case of an rvalue T = std::tuple<Obj, Obj&, Obj&&>&& object tuple:

  • Move first element (Obj)
  • Don't move second element (Obj&)
  • Move third element (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

Oktalist
Oktalist

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

Barry
Barry

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

Related Questions