Home > database >  Calling a common method of tuple elements
Calling a common method of tuple elements

Time:07-06

Say I have a tuple of types T1,...,TN that implement some method, apply().

How do I define a function that takes this tuple and some initial element, and returns the chained call of apply() on this element?

For example:

template <typename... Args, typename Input>
auto apply(std::tuple<Args...> const &tpl, Input x) {
  // return ???
}

// simple example

struct Sqr {
  static int apply(int x) { return x * x; }
};

struct AddOne {
  static int apply(int x) { return x   1; }
};

void test() {
  auto tpl1 = std::make_tuple(Sqr{}, AddOne{}, AddOne{});
  auto tpl2 = std::make_tuple(AddOne{}, AddOne{}, Sqr{});
  assert(apply(tpl1, 2) == 16);
  assert(apply(tpl2, 2) == 6);
}

I tried to use fold expressions, and variations of answers from: Template tuple - calling a function on each element but couldn't get anything to compile.

The main difference is that I need each invocation's result as the input for the next one.

Concretely, I tried the following, which failed because it calls each argument with the initial value:

template <typename... Args, typename Input>
auto apply(std::tuple<Args...> const &tpl, Input x) {
  return std::apply([&x](auto &&... args) {
    return (..., args.apply(x));
  }, tpl);
}

EDIT: To clarify, I want the methods to be called in a specific order - last to first - similarly to mathematical function composition.

(f * g)(x) := f(g(x))

CodePudding user response:

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

CodePudding user response:

The main difference is that I need each invocation's result as the input for the next one.

Apply fold-expression to assignment operator

template <typename... Args, typename Input>
auto my_apply(std::tuple<Args...> const &tpl, Input x) {
  return std::apply([&x](auto... op) {
    return ((x = op.apply(x)), ...);
  }, tpl);
}

Demo

You can introduce an dummy variable for reverse order

template <typename... Args, typename Input>
auto my_apply(std::tuple<Args...> const &tpl, Input x) {
  return std::apply([&x](auto... op) {
    int dummy;
    (dummy = ... = ((x = op.apply(x)), 0));
    return x;
  }, tpl);
}

Demo

  • Related