6

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.

What I got so far.

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.

Questions

This implementation works. However, I do have a few questions at this stage.

  1. Is there any way, I can add perfect-forward to this function? And does that even make sense? Or should I declare those arrays const-references?
  2. The assumption so far is, that the return-type of the reduction is 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, ...>.
Community
  • 1
  • 1
Lemming
  • 4,085
  • 3
  • 23
  • 36
  • This looks a lot like std::accumulate... – Mooing Duck Feb 17 '14 at 22:45
  • I really can't understand the purpose of such a thing – Mooing Duck Feb 17 '14 at 23:00
  • @MooingDuck only it's **`constexpr`**. – iavr Feb 17 '14 at 23:23
  • [BAM](http://coliru.stacked-crooked.com/a/a9dae23f802225fa) – Mooing Duck Feb 18 '14 at 00:01
  • @MooingDuck That's a very nice implementation. Can you think of any downsides/upsides compared iavr's answer and what I put [here](https://ideone.com/r7oatq). – Lemming Feb 18 '14 at 22:37
  • @MooingDuck About the purpose. I'm experimenting with N-dimensional arrays. For this it can be necessary (or at least nice) to be able to calculate a shape, or stride, or size at compile time. – Lemming Feb 18 '14 at 22:39
  • @Lemming: _technically_ mine has undefined behavior at one point. It you examine line 10 carefully you can see that it calls a functionoid and moves from the functionoid without a sequence point between. C++1y will add the ability to fix it: http://coliru.stacked-crooked.com/a/43b1cd5841dca9b7 As to other pros and cons, I haven't the foggiest how yours works, and I haven't examined Iavr's. Mine is simpler and more flexible, but harder on the compiler. – Mooing Duck Feb 18 '14 at 23:07
  • @MooingDuck Fair enough, it is definitely simpler. I guess for now I'm going to stick with my version from below, because it supports both array reduction, and reduction of variadic args. It's nice to have some flexibility... – Lemming Feb 19 '14 at 13:48

1 Answers1

2

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.

iavr
  • 7,547
  • 1
  • 18
  • 53
  • @KonradRudolph Sorry, one more definition: `#define INLINE __attribute__((always_inline))` in Clang, `#define INLINE __forceinline` in GCC. Could just be `inline` or removed. In practice I don't get as much inlining as I'd like without it, especially with GCC. Meaning large executables. – iavr Feb 17 '14 at 23:44
  • @iavr Thanks for your detailed answer. I spent some time trying to adapt my solution. You can see the result [here](https://ideone.com/r7oatq). It runs, and produces the expected output. However, I have a few questions about it. In the `array_reduce` implementation (line 80) is that the right way to explicitely forward `&&`-ness of an array. I.e. a `const A&`, and a `A&&` overload. And in line 130, is there a way to write `arg_sum(1,2,3)` instead of `arg_sum()(1,2,3)` (pure cosmetics...). And in general, what do you think of this code, any suggestions for improvement? Thanks for your help! – Lemming Feb 18 '14 at 22:09
  • @iavr Another thing, why is `copy`, or `std::decay` necessary? I can see that a reference return value would be bad. But, why would you want to convert arrays to pointers? – Lemming Feb 18 '14 at 22:26
  • @Lemming line 80: what you have is just fine, one overload for `const array <...>&` and another for `array <...>&&`, which needs `std::move`. I generally prefer the universal version `A&&` along with an `enable_if` and an `std::forward` as in my answer because it only takes one function, so it's less verbose. In the most general case you'd a third overload for `array <...>&` (non-const), and I feel it's too much to write everything 3 times. – iavr Feb 18 '14 at 23:15
  • @iavr (line 80) Good to know, thanks. About the universal `A&&` with `enable_if`: I don't know how to test if it's a `std:array` and extract type and number without adding a lot of boiler plate. All the `type_traits` tools seem to be written only with `T[]` arrays in mind. – Lemming Feb 18 '14 at 23:24
  • @Lemming line 130: What I do in practice is I put all function object types in a namespace say `fun`, then outside I say `static __attribute__ ((unused)) fun::sum sum;`. So out there `sum` is an *instance* and I can call it like `sum(1, 2, 3)` which really looks like a regular function call. Plus, I can use this `sum` itself as an object to pass it e.g. as argument to other functions. The attribute is so that you don't get a warning if this instance is never used. There's only one empty instance per function in your library namespace so I guess this doesn't do much harm to your executable. – iavr Feb 18 '14 at 23:26
  • @Lemming (line 80, traits) e.g. `template struct is_fixed_array : false_type { }; template struct is_fixed_array > : true_type { }; ` – iavr Feb 18 '14 at 23:30
  • @Lemming (line 80, traits) For the size: `template struct array_size; template struct array_size > : std::integral_constant { };` – iavr Feb 18 '14 at 23:31
  • @Lemming (`copy/std::decay`) You can safely forget about it. The main objective is to remove a reference--I can't imagine such a reduce function applied on a plain array or function. But `common_type` removes references anyway. `copy` is a more general recursive thing, converting e.g. a `tuple ` to `tuple ` or an `indirect_array <...>` to `array <...>` (these are my own structures) so you get a deep copy no matter what. I guess this is too much for your needs. – iavr Feb 18 '14 at 23:46
  • @Lemming One more thing: I find name `arg_sum` a bit unfortunate in the sense that `arg_max` would resemble the [argument of maximum](http://en.wikipedia.org/wiki/Argmax) – iavr Feb 18 '14 at 23:54
  • @Lemming OOPS be careful (line 59): You still need to `remove_reference/remove_cv` from return type `Arg`. This is universal so could match e.g. an `int&`. Are you sure you want to return this reference? I think forwarding should stop there. Similarly for `id` at (line 27). – iavr Feb 19 '14 at 00:07
  • @iavr Thanks for all the input, and for pointing out the return reference issue. I applied the suggested fixes. The final code is [here](https://ideone.com/KVPJYi0) – Lemming Feb 19 '14 at 13:42