2
\$\begingroup\$

How might chain_apply() be improved?

auto chain_apply(auto&& a, auto&& ...f)
{
  auto const execute([&]<auto I>(auto&& self)
    {
      if constexpr(I)
      {
        return std::get<I>(
          std::forward_as_tuple(f...))(
          self.template operator()<I - 1>(self)
        );
      }
      else
      {
        return std::apply(
          std::get<0>(std::forward_as_tuple(f...)),
          std::forward<decltype(a)>(a)
        );
      }
    }
  );

  return execute.template operator()<sizeof...(f) - 1>(execute);
}

Usage:

int main()
{
  auto const inc([](auto const i) noexcept
    {
      return i + 1;
    }
  );

  std::cout << chain_apply(std::tuple(0), inc, inc, inc, inc) << std::endl;

  return 0;
}

EDIT:

No pipe operator this time, but consider:

std::tuple(0) | inc | inc | inc | inc;
\$\endgroup\$
1
  • \$\begingroup\$ here is a more complete version, with the possibility of void and tuple<> returns (tuple<> returns are auto-applied to the next function in the chain). \$\endgroup\$ Commented Feb 11, 2022 at 21:04

1 Answer 1

2
\$\begingroup\$

A simpler approach

You can do this without packing the callables into a tuple and without using a recursive lambda. Consider:

auto chain_apply(auto&& a, auto&& f, auto&& ...fs)
{
  if constexpr(sizeof...(fs)) {
      return chain_apply(std::forward_as_tuple(std::apply(f, a)), fs...);
  } else {
      return std::apply(f, a);
  }
}

(Some std::forward()s omitted for the sake of clarity.)

It's not very useful to chain std::apply()

std::apply() is used to unpack a tuple and apply it to a callable. However, the return value of most functions you would want to use is not a std::tuple, instead it is a single value. So you can forward that as a tuple of one element, but that means you can only chain multiple callables if each one after the first only takes a single parameter.

If the function you pass does return a std::tuple, you pack that tuple in another tuple. You could make chain_apply() detect if the return value after applying a callable is a tuple, and if so not wrap it but pass it to the next function, but that seems like a heuristic that might fail in some situations.

If it only makes sense for a to be a tuple with one element, then you could ask yourself if it needed to be a std::tuple to begin with. I would also much rather be able to write:

std::cout << chain_apply(0, inc, inc, inc, inc) << '\n';
\$\endgroup\$
3
  • \$\begingroup\$ In my defense, std::apply() could also be improved in this way. \$\endgroup\$ Commented Feb 12, 2022 at 1:55
  • \$\begingroup\$ Well, there's std::invoke() if you don't want to pass a std::tuple. Maybe all this will be moot in C++23 or C++26 if we get UFCS. \$\endgroup\$
    – G. Sliepen
    Commented Feb 12, 2022 at 9:45
  • \$\begingroup\$ Oh no, I'd like to chain coroutines :) That's what I'm working on. \$\endgroup\$ Commented Feb 12, 2022 at 10:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.