I want to write a constexpr function, that reduces a given std::array
with a binary operation. I.e. a function which implements
template <typename T, std::size_t N>
reduce(std::array<T, N>, binary_function);
To keep things simple I want to start with addition. E.g.
sum(std::array<int, 5>{{1,2,3,4,5}}); // returns 15.
I use the usual indexing trick to index array elements. I.e. generate a int
sequence, that can be used for indexing with parameter list-expansion.
template <int... Is>
struct seq {};
template <int I, int... Is>
struct gen_seq : gen_seq<I - 1, I - 1, Is...> {};
template <int... Is>
struct gen_seq<0, Is...> : seq<Is...> {}; // gen_seq<4> --> seq<0, 1, 2, 3>
The sum
function is then defined through variadic template recursion.
// The edge-condition: array of one element.
template <typename T>
constexpr T sum(std::array<T, 1> arr, decltype(gen_seq<0>{})) {
return std::get<0>(arr);
}
// The recursion.
template <typename T, std::size_t N, int... Is>
constexpr auto sum(std::array<T, N> arr, seq<Is...>) -> decltype(T() + T()) {
return sum(std::array<T, N - 1>{ { std::get<Is>(arr)... } },
gen_seq<N - 2>()) +
std::get<N - 1>(arr);
}
// The interface - hides the indexing trick.
template <typename T, std::size_t N>
constexpr auto sum(std::array<T, N> arr)
-> decltype(sum(arr, gen_seq<N - 1>{})) {
return sum(arr, gen_seq<N - 1>{});
}
Here you can see it in action.
This implementation works. However, I do have a few questions at this stage.
decltype(T()+T())
. I.e. what you get when you add two elements. While this should be true for addition in most cases, it might no longer be true for a general reduction. Is there a way, of getting the type of a[0] + (a[1] + (a[2] + ... ) )
? I tried something like this, but I don't know how I can produce a template parameter list of <T, T, T, ...>
.My answer is based on my own implementation of such staff.
I prefer the general reduce (or fold, or accumulate) function to operate directly on elements as its own function arguments rather than being within a container like std::array
. This way, instead of constructing a new array in every recursion, elements are passed as arguments and I guess the whole operation is easier for the compiler to inline. Plus it's more flexible, e.g. could be used directly or on the elements of a std::tuple
. The general code is here. I repeat here the main function:
template <typename F>
struct val_fold
{
// base case: one argument
template <typename A>
INLINE constexpr copy <A>
operator()(A&& a) const { return fwd<A>(a); }
// general recursion
template <typename A, typename... An>
INLINE constexpr copy <common <A, An...> >
operator()(A&& a, An&&... an) const
{
return F()(fwd<A>(a), operator()(fwd<An>(an)...));
}
};
I am sorry this is full of my own definitions, so here is some help: F
is the function object defining the binary operation. copy
is my generalization of std::decay
that recurses within arrays and tuples. fwd
is just a shortcut for std::forward
. Similarly, common
is just a shortcut for std::common_type
but intended for a similar generalization (in general, each operation may yield an expression template for lazy evaluation and here we are forcing evaluation).
How would you define sum
using the above? First define the function object,
struct sum_fun
{
template <typename A, typename B>
INLINE constexpr copy <common <A, B> >
operator()(A&& a, B&& b) const { return fwd<A>(a) + fwd<B>(b); }
};
then just
using val_sum = val_fold<sum_fun>;
How would you call this when starting with an std::array
? Well, once you've got your Is...
, all you need is
val_sum()(std::get<Is>(arr)...);
which you may wrap within your own interface. Note that in C++14, std::array::operator[]
is constexpr, so this would just be
val_sum()(arr[Is]...);
Now, to your questions:
1) Forwarding: Yes, std::get
is forwarding array elements into val_sum
, which is recursively forwarding everything to itself. So all that remains is your own interface to forward the input array, e.g.
template <typename A, /* enable_if to only allow arrays here */>
constexpr auto sum(A&& a) -> /* return type here */
{
return sum(std::forward<A>(a), gen_seq_array<A>{});
}
and so on, where gen_seq_array
would take the raw type of A (std::remove_ref
, std::remove_cv
etc.), deduce N
, and call gen_seq<N>{}
. Forwarding makes sense if array elements have move semantics. It should be everywhere, e.g. the call of val_sum
above would be something like
val_sum()(std::get<Is>(std::forward<A>(a))...);
2) Return type: As you have seen, I am using std::common_type
as the return type, which should do for sum
and most common arithmetic operations. This is already variadic. If you'd like your own type function, it's easy to make a variadic out of a binary type function, using template recursion.
My own version of common
is here. It's a bit more involved, but it is still a recursive template containing some decltype
to do the actual work.
In any case, this is more general than what you need, because it's defined for an arbitrary number of any given types. You only have one type T
in your array, so what you have should be enough.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With