Search code examples
c++tuplesc++17template-meta-programmingperfect-forwarding

Perfectly transform a 2-tuple into a pair


I want an utility to transform a std::tuple<T,U> to std::pair<T,U>, while leaving a std::tuple<T, U, V, W...> unchanged.

Furthermore, I want this utility to

  • be a function object, not a function, so that I can pass it around as I like;
  • enforce that it's input must be a std::tuple;
  • taking advantage of move semantics whenever it is possible to do so and it makes sense.

The last point is the difficult part, for me, and it's the reason I'm asking this question.


I know that tuples can store values as well as references (lvalue references as well as rvalue references), so imagine that the sought function, if fed with an rvalue 2-tuple, it could steal resources from the non-reference components, but should leave intact the reference component.

For instance given

A a;
B b;
std::tuple<A, B&> t{a,b};

where I know a is copied in the tuple, while b is not, I think that doing

auto p{to_pair(t)};

should result in a std::pair<A, B&> which holds another copy of a and a reference to the only b that exists so far.

On the other hand, doing

B b;
auto p{to_pair(std::tuple<A, B&>{A{},b})};

should result again in a std::pair<A, B&>, but this would hold the very A{} of which no copy should ever be made; and it would hold a reference to b anyway.


The scenario described above is already enough for me to be in doubt about what to do, let alone thinking of other combinations of &/const&/&&.

Some time ago I had a hard time answering a question about std::tuples and perfect forwarding, but I can never say I've understood them fully.


Solution

  • template<class...Fs>
    struct overloaded : Fs... {
      using Fs::operator()...;
    };
    template<class...Fs>
    overloaded(Fs&&...)->overloaded<std::decay_t<Fs>...>;
    
    auto move_tuples = []<class...Ts>(std::tuple<Ts...> x){return std::move(x);};
    auto to_pair = []<class A, class B>(std::tuple<A, B> x){
      return std::pair<A,B>( std::get<0>(std::move(x)), std::get<1>(std::move(x)) );
    };
    auto your_function = overloaded{move_tuples, to_pair};
    

    I think that is what you want.

    I take the arguments by value, then move them to the return value. If they are values, the result is that the contained values are moved. If they are references, they aren't, the references are just propagated.

    The get<N> "does the right thing" when passed an rvalue tuple. If returns an rvalue from a value.

    In you'll have to replace the lambdas with manual structs.

    Or write two normal overloads, and

    auto obj=[](auto&&x)->decltype(normal_func(decltype(x)(x))){return decltype(x)(x);};
    

    to turn the overloaded function call normal_func into a single function object.