C++ Transform a std::tuple<A, A, A...> to a std::vector or std::deque
Asked Answered
F

3

10

In the simple parser library I am writing, the results of multiple parsers is combined using std::tuple_cat. But when applying a parser that returns the same result multiple times, it becomes important to transform this tuple into a container like a vector or a deque.

How can this be done? How can any tuple of the kind std::tuple<A>, std::tuple<A, A>, std::tuple<A, A, A> etc be converted into a std::vector<A>?

I think this might be possible using typename ...As and sizeof ...(As), but I am not sure how to create a smaller tuple to call the function recursively. Or how to write an iterative solution that extracts elements from the tuple one by one. (as std::get<n>(tuple) is constructed at compile-time).

How to do this?

Furgeson answered 27/2, 2017 at 20:2 Comment(2)
Possible duplicate of iterate over tupleAnhwei
Totally different. Much better approached with the indices trick.Arraign
L
19

With the introduction of std::apply(), this is very straightforward:

template <class Tuple,
   class T = std::decay_t<std::tuple_element_t<0, std::decay_t<Tuple>>>>
std::vector<T> to_vector(Tuple&& tuple)
{
    return std::apply([](auto&&... elems){
        return std::vector<T>{std::forward<decltype(elems)>(elems)...};
    }, std::forward<Tuple>(tuple));
}

std::apply() is a C++17 function but is implementable in C++14 (see link for possible implementation). As an improvement, you could add either SFINAE or a static_assert that all the types in the Tuple are actually T.


As T.C. points out, this incurs an extra copy of every element, since std::initializer_list is backed by a const array. That's unfortunate. We win some on not having to do boundary checks on every element, but lose some on the copying. The copying ends up being too expensive, an alternative implementation would be:

template <class Tuple,
   class T = std::decay_t<std::tuple_element_t<0, std::decay_t<Tuple>>>>
std::vector<T> to_vector(Tuple&& tuple)
{
    return std::apply([](auto&&... elems) {
        using expander = int[];

        std::vector<T> result;
        result.reserve(sizeof...(elems));
        expander{(void(
            result.push_back(std::forward<decltype(elems)>(elems))
            ), 0)...};
        return result;
    }, std::forward<Tuple>(tuple));
}

See this answer for an explanation of the expander trick. Note that I dropped the leading 0 since we know the pack is non-empty. With C++17, this becomes cleaner with a fold-expression:

    return std::apply([](auto&&... elems) {
        std::vector<T> result;
        result.reserve(sizeof...(elems));
        (result.push_back(std::forward<decltype(elems)>(elems)), ...);
        return result;
    }, std::forward<Tuple>(tuple));

Although still relatively not as nice as the initializer_list constructor. Unfortunate.

Lights answered 27/2, 2017 at 20:26 Comment(5)
Plus 1 not only for the great answer but mainly for for the assert advice.Sunil
The only thing I dislike is the initializer list, which is one extra copy per element.Gally
@Gally Oh you're right. I didn't think of that. That's unfortunate... I guess there's reserve()/push_back()?Lights
@Gally Added a non-initializer-list answer. I'm pretty unsatisfied with this now :(Lights
There's no real need to do the void cast unless you are extra paranoid - in which case you forgot to cast in the fold case. An intermediate approach might be to use an explicit T array and move_iterator.Gally
M
3

Here's one way to do it:

#include <tuple>
#include <algorithm>
#include <vector>
#include <iostream>

template<typename first_type, typename tuple_type, size_t ...index>
auto to_vector_helper(const tuple_type &t, std::index_sequence<index...>)
{
    return std::vector<first_type>{
        std::get<index>(t)...
            };
}

template<typename first_type, typename ...others>
auto to_vector(const std::tuple<first_type, others...> &t)
{
    typedef typename std::remove_reference<decltype(t)>::type tuple_type;

    constexpr auto s =
        std::tuple_size<tuple_type>::value;

    return to_vector_helper<first_type, tuple_type>
        (t, std::make_index_sequence<s>{});
}

int main()
{
    std::tuple<int, int> t{2,3};

    std::vector<int> v=to_vector(t);

    std::cout << v[0] << ' ' << v[1] << ' ' << v.size() << std::endl;
    return 0;
}
Milfordmilhaud answered 27/2, 2017 at 20:19 Comment(0)
K
0

Although, this doesn't answer the question completely, This still might be suitable in some cases. Only when the number of elements in tuple is around 5, 6. (And you know the size).

tuple<int, int, int, int> a = make_tuple(1, 2, 3, 4);
auto [p, q, r, s] = a;
vector<int> arr(p, q, r, s); // Now, arr has the same elements as in tuple a

Note that, this is C++ 17 feature. More info here

Kylix answered 10/6, 2021 at 14:10 Comment(1)
the variables p, q, r and s are "probably" wasted. You should also use init-list for vector not parenthesis.Gall

© 2022 - 2024 — McMap. All rights reserved.