There may be snazzier C++17 ways of doing it, but there is always good old-fashioned partially-specialized recursion. We'll make a struct
that represents your recursive algorithm, and then we'll build a function wrapper around that struct
to aid in type inference. First, we'll need some imports.
#include <tuple>
#include <utility>
#include <iostream> // Just for debugging later :)
Here's our structure definition.
template <typename Input, typename... Ts>
struct ApplyOp;
Not very interesting. It's an incomplete type, but we're going to provide specializations. As with any recursion, we need a base case and a recursive step. We're inducting on the tuple elements (you're right to think of this as a fold-like operation), so our base case is when the tuple is empty.
template <typename Input>
struct ApplyOp<Input> {
Input apply(Input x) {
return x;
}
};
In this case, we just return x
. Computation complete.
Now our recursive step takes a variable number of arguments (at least one) and invokes .apply
.
template <typename Input, typename T, typename... Ts>
struct ApplyOp<Input, T, Ts...> {
auto apply(Input x, const T& first, const Ts&... rest) {
auto tail_op = ApplyOp<Input, Ts...>();
return first.apply(tail_op.apply(x, rest...));
}
};
The tail_op
is our recursive call. It instantiates the next version of ApplyOp
. There are two apply
calls in this code. first.apply
is the apply
method in the type T
; this is the method you control which determines what happens at each step. The tail_op.apply
is our recursive call to either another version of this apply
function or to the base case, depending on what Ts...
is.
Note that we haven't said anything about tuples yet. We've just taken a variadic parameter pack. We're going to convert the tuple into a parameter pack using an std::integer_sequence
(More specifically, an std::index_sequence
). Basically, we want to take a tuple containing N
elements and convert it to a sequence of parameters of the form
std::get<0>(tup), std::get<1>(tup), ..., std::get<N-1>(tup)
So we need to get an index sequence from 0
up to N-1
inclusive (where N-1
is our std::tuple_size
).
template <typename Input, typename... Ts>
auto apply(const std::tuple<Ts...>& tpl, Input x) {
using seq = std::make_index_sequence<std::tuple_size<std::tuple<Ts...>>::value>;
// ???
}
That complicated-looking type alias is building our index sequence. We take the tuple's size (std::tuple_size<std::tuple<Ts...>>::value
) and pass it to std::make_index_sequence
, which gives us an std::index_sequence<0, 1, 2, ..., N-1>
. Now we need to get that index sequence as a parameter pack. We can do that with one extra layer of indirection to get type inference.
template <typename Input, typename... Ts, std::size_t... Is>
auto apply(const std::tuple<Ts...>& tpl, Input x, std::index_sequence<Is...>) {
auto op = ApplyOp<Input, Ts...>();
return op.apply(x, std::get<Is>(tpl)...);
}
template <typename Input, typename... Ts>
auto apply(const std::tuple<Ts...>& tpl, Input x) {
using seq = std::make_index_sequence<std::tuple_size<std::tuple<Ts...>>::value>;
return apply(tpl, x, seq());
}
The second apply
is the one outside users call. They pass a tuple and an input value. Then we construct an std::index_sequence
of the appropriate type and pass that to the first apply
, which uses that index sequence to access each element of the tuple in turn.
Complete, runnable example