6

Suppose I have a std::vector of a size known at compile time, and I want to turn that into an std::array. How would I do that? Is there a standard function to do this?

The best solution I have so far is this:

template<class T, std::size_t N, class Indexable, std::size_t... Indices>
std::array<T, N> to_array_1(const Indexable& indexable, std::index_sequence<Indices...>) {
  return {{ indexable[Indices]... }};
}

template<class T, std::size_t N, class Indexable>
std::array<T, N> to_array(const Indexable& indexable) {
  return to_array_1<T, N>(indexable, std::make_index_sequence<N>());
}

std::array<Foo, 123> initFoos {
  std::vector<Foo> lst;
  for (unsigned i = 0; i < 123; ++i)
    lst.push_back(getNextFoo(lst));
  return to_array<Foo, 123>(lst); // passing lst.begin() works, too
}

The application is similar to Populate std::array with non-default-constructible type (no variadic templates): I too have a type which is not default-constructible, so I need to compute the actual values by the time the array gets initialized. However contrary to that question, for me the values are not merely a function of the index, but also of the preceding values. I can build my values much more easily using a loop than a series of function calls. So I construct the elements in a loop and place them in a vector, then I want to use the final state of that vector to initialize the array.

The above seems to compile and work fine, but perhaps there are ways to improve it.

  1. Perhaps I could make clever use of some standard library functionality that I wasn't aware of.
  2. Perhaps I can avoid the helper function somehow.
  3. Perhaps I can somehow formulate this in such a way that it works with move semantics for the elements instead of the copy semantics employed above.
  4. Perhaps I can avoid the random access using operator[], and instead use forward iterator semantics only so it would work for std::set or std::forward_list as input, too.
  5. Perhaps I should stop using std::vector and instead express my goal using std::array<std::optional<T>, N> instead, using C++17 or some equivalent implementation.

Related questions:

Community
  • 1
  • 1
MvG
  • 57,380
  • 22
  • 148
  • 276
  • 1
    Interesting problem, but this smells bad. Why would one want to generate the data only to then copy them? This is forced by your desire to pack them in a `std::array`, but what's the advantage of that over the original `std::vector`? Both keep the data in a consecutive piece of memory, so there should be no advantage of the former over the latter. The only difference is that `size` is a compile-time variable, but that is the case anyway. – Walter Dec 02 '16 at 11:30
  • @Walter: Valid question. On the one hand, my original is a `std::set` so I need to convert at least once anyway. And I'm using a conversion constructor to perform a type conversion for the elements, too. So it's not a matter of just keeping the `std::vector`. I could use one, though. Main argument against is probably my feeling that the more the compiler knows, the better it can optimize. I'm dealing with just 24 elements in practice, and although that might be too much for complete loop unrolling, it could e.g. unroll groups of four, knowing that the count is divisible by 4. Just a feeling. – MvG Dec 02 '16 at 12:26

1 Answers1

5

I would propose:

template<typename T, typename Iter, std::size_t... Is>
constexpr auto to_array(Iter& iter, std::index_sequence<Is...>)
-> std::array<T, sizeof...(Is)> {
    return {{ ((void)Is, *iter++)... }};
}

template<std::size_t N, typename Iter,
         typename T = typename std::iterator_traits<Iter>::value_type>
constexpr auto to_array(Iter iter)
-> std::array<T, N> {
    return to_array<T>(iter, std::make_index_sequence<N>{});
}

This deduces the element type from the iterator and leaves copy-vs-move semantics up to the caller – if the caller wants to move, they can opt-in via std::move_iterator or the like:

auto initFoos() {
    constexpr unsigned n{123};

    std::vector<Foo> lst;
    for (unsigned i{}; i != n; ++i) {
        lst.push_back(getNextFoo(lst));
    }

    // copy-init array elements
    return to_array<n>(lst.cbegin());

    // move-init array elements
    return to_array<n>(std::make_move_iterator(lst.begin()));
}

Online Demo


EDIT: If one wants to override the deduced element type, as indicated in the comments, then I propose:

template<typename T, typename Iter, std::size_t... Is>
constexpr auto to_array(Iter& iter, std::index_sequence<Is...>)
-> std::array<T, sizeof...(Is)> {
    return {{ ((void)Is, T(*iter++))... }};
}

template<std::size_t N, typename U = void, typename Iter,
         typename V = typename std::iterator_traits<Iter>::value_type,
         typename T = std::conditional_t<std::is_same<U, void>{}, V, U>>
constexpr auto to_array(Iter iter)
-> std::array<T, N> {
    return to_array<T>(iter, std::make_index_sequence<N>{});
}

This leaves the element type optional but makes it the second parameter rather than the first, so usage would look like to_array<N, Bar>(lst.begin()) rather than to_array<Bar, N>(lst.begin()).

ildjarn
  • 62,044
  • 9
  • 127
  • 211
  • 1
    What is the order of evaluation of multiple `iter++` expressions there? – Maxim Egorushkin Dec 02 '16 at 11:41
  • 3
    @MaximEgorushkin : All expansions inside of a brace-init-list are guaranteed to be executed left-to-right. – ildjarn Dec 02 '16 at 11:42
  • 1
    Thanks! This tackles points 3 and 4 of my list, and I hadn't been aware of the move iterator. In my actual application I am making use of an explicit conversion constructor, initializing `array` from `vector` using `Bar::Bar(const Foo&)` and perhaps `Bar::Bar(Foo&&)` in the future. It turns out that the code you gave doesn't count as explicit conversion, while my original one did. So I added a `T(…)` constructor invocation in there. – MvG Dec 02 '16 at 11:52