(By the way, C++ Weekly - Ep 269 - How To Use C++20's constexpr std::vector and std::string is very relevant!)
The output size can't be calculated separately from the actual computation.
Good, but since it is at compile time, you can do all of that with std::integer_sequence (or std::index_sequence, if the type of the elements is std::size_t) and some metaprogramming.
Then, if you really need a std::array, you can make the conversion with this:
template<typename N, N ...ns>
consteval auto seq2array(std::integer_sequence<N, ns...>) {
return std::array{ns...};
}
static_assert(seq2array(std::index_sequence<1, 2, 3>{}) == std::array<std::size_t, 3>{1, 2, 3});
Now, how to generate the std::index_sequence depends on what the "complex calculation" is.
Weijun Zhou suggested in a comment a toy example:
make_vector(int n) can be a function that returns a vector containing all non-negative numbers k not greater than n that satisfy a predicate f(k), where f is a very expensive function.
What does it take to do that?
Well, that just means filtering the sequence of numbers from 0 inclusive to n exclusive, with the predicate f.
Let's invent the predicate
constexpr auto f = [](auto k) {
// expensive compuptation
return k % 2 == 0;
};
and write a filtering utility. For the latter we need a recursive approach where we apply the predicate to the first element of the sequence to decide whether to keep it or not, make a singleton or empty sequence out of it respectively, and concatenate with the rest of the filtered sequence.
So the first thing we need is actually a way to concatenate sequences. Here it is:
template<typename N, N ...ns, N ...ms>
consteval auto cat(std::integer_sequence<N, ns...>, std::integer_sequence<N, ms...>) {
return std::integer_sequence<N, ns..., ms...>{};
}
static_assert(std::is_same_v<decltype(cat(std::index_sequence<1, 2>{}, std::index_sequence<3, 4>{})), std::index_sequence<1, 2, 3, 4>>);
static_assert(std::is_same_v<decltype(cat(std::index_sequence<1, 2>{}, std::index_sequence<>{})), std::index_sequence<1, 2>>);
static_assert(!std::is_same_v<decltype(cat(std::index_sequence<2, 2>{}, std::index_sequence<3, 4>{})), std::index_sequence<1, 2, 3, 4>>);
Finally, we need the filtering utility.
template<typename Seq, typename Pred>
struct Filter {};
template<typename N, N n, N ... ns, typename Pred>
struct Filter<std::integer_sequence<N, n, ns...>, Pred> {
using head = std::integer_sequence<N, n>;
using tail = typename Filter<std::integer_sequence<N, ns...>, Pred>::type;
using type = std::conditional_t<Pred{}(n), decltype(cat(head{}, tail{})), tail>;
};
template<typename N, typename Pred>
struct Filter<std::integer_sequence<N>, Pred> {
using head = std::integer_sequence<N>;
using type = head;
};
template<typename N, N ...ns, typename Pred>
consteval auto filter(std::integer_sequence<N, ns...>, Pred) {
return typename details::Filter<std::integer_sequence<N, ns...>, Pred>::type{};
}
static_assert(std::is_same_v<decltype(filter(std::index_sequence<>{}, f)), std::index_sequence<>>);
static_assert(std::is_same_v<decltype(filter(std::index_sequence<1, 2, 3, 4>{}, f)), std::index_sequence<2, 4>>);
With all of that in place, our make_vector, which I'm renaming make_array can be written like this
template<std::size_t n>
consteval auto make_array() {
return seq2array(filter(std::make_integer_sequence<std::size_t, n>{}, f));
}
and used like this
static_assert(make_array<10>() == std::array<std::size_t, 5>{0, 2, 4, 6, 8});
Notice that the abstractions filter and cat defined above are very general, and could be used in many other cases, so they can be part of a library. That would justify the amount of boilerplate, which is anyway not that much.
make_vectoris a complex calculation, it's just a one-liner in the example above.constexprstructured bindings, such asauto [vec, constexpr size] = make_vector();