2

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; }
};

enum class Choice {
  One,
  Two,
};

struct Choose {
  static int apply(Choice choice) {
    switch (choice) {
    case Choice::One:
      return 1;
    case Choice::Two:
      return 2;
    }
  }
};

void test() {
  auto tpl = std::tuple(Sqr{}, Choose{});
  assert(apply(tpl, Choice::One) == 1);
  assert(apply(tpl, Choice::Two) == 4);
}

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);
}

Clarifications and assumptions:

  • 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))
    
  • The input and output types of each tuple argument are not constricted. The only assumption is that consecutive arguments agree on the corresponding types.
cigien
  • 57,834
  • 11
  • 73
  • 112
ransh
  • 41
  • 1
  • 6
  • I think you have to make all the types to derive from the same base class which implement `apply()` – Marco Beninca Jul 05 '22 at 14:45
  • @MarcoBeninca How would this help? Note that I can't store these types in a vector, since they may be different, and I want to avoid dynamic dispatch. – ransh Jul 05 '22 at 14:50
  • 1
    Show what you've tried and the errors you encountered. https://stackoverflow.com/a/6894436/5494370 is likely similar to what you want – Alan Birtles Jul 05 '22 at 14:54
  • 1
    You were almost there. Just make it `return ((x = args.apply(x)), ...);` [Demo](https://godbolt.org/z/M99MfjfEW) – Igor Tandetnik Jul 05 '22 at 15:20
  • 1
    One issue is that `std::apply` exists, and argument-dependent lookup will try it if visible, since the `std::tuple` argument's type is in the same namespace. So that could cause a failure even if your own `apply` is correct. – aschepler Jul 05 '22 at 15:27
  • I came up with this: `template auto myapply(std::tuple const& t, Input const& x) { if constexpr (index == sizeof...(Args)) return x; else return std::get(t).apply(myapply(t, x)); }` – Raymond Chen Jul 05 '22 at 15:28
  • 1
    You want to evaluate last to first, or the asserts are mixed up? – aschepler Jul 05 '22 at 15:29
  • Thanks for the comments, I added a clarification on the evaluation order to the original post. – ransh Jul 05 '22 at 15:59
  • Related to [compose-lambda-function-in-c-using-parameter-pack](https://stackoverflow.com/questions/58202940/compose-lambda-function-in-c-using-parameter-pack) – Jarod42 Jul 06 '22 at 09:10

3 Answers3

2

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

康桓瑋
  • 33,481
  • 5
  • 40
  • 90
  • This is cool. Is there a way to call in reverse order? I saw you modified the test function to match the invocation order in practice, rather than one that I specified. – ransh Jul 05 '22 at 15:51
  • I think the reverse order wouldn't work for mixed-type tuples, would it? – ransh Jul 05 '22 at 16:01
  • @ransh. No, it still works. – 康桓瑋 Jul 05 '22 at 16:03
  • I don't think it does: https://godbolt.org/z/K9nrqeW8b (the original solution also doesn't compile, since there's no viable assignment between these types). – ransh Jul 05 '22 at 16:18
  • That requires that return types are all `Input` (`Input T::apply(Input)`), you cannot do `my_apply(std::tuple{LenghtOp, ToStringOp, SquareOp}, 42)`. – Jarod42 Jul 05 '22 at 21:51
2

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

Silvio Mayolo
  • 62,821
  • 6
  • 74
  • 116
  • Thank you for this detailed and helpful answer. I think I tried too hard to find that "snazzy c++17 solution", but your solution is clear and elegant. – ransh Jul 05 '22 at 15:24
1

One way without recursion is to use fold expression.

Unfortunately, there is no call composition operator folding.

But you might create custom type and divert regular operator:

template <typename T>
struct Wrapper
{
    T t;
};

// Deduction guide, not needed in C++20
template <typename T> Wrapper(T) -> Wrapper<T>;

// Then the operator with changed semantic
template <typename T1, typename T2>
auto operator+(const Wrapper<T1>& lhs, const Wrapper<T2>& rhs)
{
    return Wrapper{lhs.t.apply(rhs.t)};
}

template <typename T1, typename T2>
auto operator-(const Wrapper<T1>& lhs, const Wrapper<T2>& rhs)
{
    return Wrapper{rhs.t.apply(lhs.t)};
}

// And now, the function with fol expression

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

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

Usage similar to

// std::size(std::to_string(10 * 10));
my_apply(std::tuple{ LengthOp{}, ToStringOp{}, SquareOp{}}, 10);
my_apply_rev(std::tuple{ SquareOp{}, ToStringOp{}, LengthOp{}}, 10);

Demo

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • Thanks. As a side note, is there something inherently wrong or unefficient with template recursion? It doesn't seem harder to understand than this approach, and I wonder about run / compile time implications of using each approach. – ransh Jul 06 '22 at 06:59
  • Template recursion tends to instantiate more template, which generally slow down compilation. For runtime, I would say it should be equivalent (actually my solution does an extra call indirection (trivial to optimize), which might slow down runtime in debug build). But as usual, for performance, you have to measure to be sure. – Jarod42 Jul 06 '22 at 08:33