An object calling multiple of functions with the correct overload set

Viewed 76

For a set of N functions fs... each taking only a single argument, I would like to create an object which has a call operator taking N arguments args..., calls all functions fs(args)... and returns outputs as a tuple.

The basic class would look something like this. I have marked the places I do not know how to implement with ???.

template <class... Fs>
struct merge_call_object {

  merge_call_object(Fs... _fs)
      : fs(std::move(_fs)...) {} 

  template <class... Args>
  auto operator()(Args &&... args) -> decltype(???){
     ???
  }

  std::tuple<Fs...> fs;
};

The expected usage of this object would be:

auto f1 = [](double x){ return 2*s; };
auto f2 = [](std::string const& s){ return s+" World!"; };

auto mco = merge_call_object{f1,f2};

// The following should yield std::tuple{42, "Hello World!"}
auto out = mco(21, "Hello "); 

So far so good, doing the above is "easy" but I want that overloading for mco works as expected i.e. the following should compile and pass

static_assert(std::is_invocable_v<decltype(mco), double, std::string> == true);
static_assert(std::is_invocable_v<decltype(mco), double, double> == false);

The biggest challenge I see is how implement correctly the SFINAE -> decltype(???).


This question is inspired by the recent talk from CppCon Overloading: The Bane of All Higher-Order Functions, around 6:40 he talks about how to wrap a function into a lambda.


My implementation without the correct overload set, plus a small test on perfect forwarding.

#include <iostream>
#include <tuple>
#include <utility>

namespace detail {
template <std::size_t... I>
constexpr auto integral_sequence_impl(std::index_sequence<I...>) {
  return std::make_tuple((std::integral_constant<std::size_t, I>{})...);
}

template <std::size_t N, typename Indices = std::make_index_sequence<N>>
constexpr auto integral_sequence = integral_sequence_impl(Indices{});

template <std::size_t N, typename Fun>
constexpr decltype(auto) apply_sequence(Fun &&fun) {
  return std::apply(std::forward<Fun>(fun), detail::integral_sequence<N>);
}

} // namespace detail

template <class... Fs>
struct merge_call_object {

  merge_call_object(Fs... _fs)
      : fs(std::move(_fs)...) {}

  template <class... Args>
  auto operator()(Args &&... args) {

    constexpr int N = sizeof...(Fs);

    auto f   = [&](auto I) { return std::get<I>(fs); };
    auto arg = [&](auto I) -> decltype(auto) {
      return std::get<I>(std::forward_as_tuple(std::forward<Args>(args)...));
    };

    return detail::apply_sequence<N>(
        [&](auto... Is) { return std::forward_as_tuple(f(Is)(arg(Is))...); });
  }

  std::tuple<Fs...> fs;
};

struct Screamer {

  Screamer() { std::cout << "Constructor!" << std::endl; }

  Screamer(Screamer &&s) { std::cout << "Move constructor!" << std::endl; }

  Screamer(Screamer const &s) { std::cout << "Copy constructor!" << std::endl; }
};

int main() {

  auto f1 = [](auto &&x) -> decltype(auto) {
    std::cout << "Calling f1" << std::endl;
    return std::forward<decltype(x)>(x);
  };

  auto f2 = [](auto &&x) -> decltype(auto) {
    std::cout << "Calling f2" << std::endl;
    return std::forward<decltype(x)>(x);
  };

  auto mco = merge_call_object{f1, f2};

  auto [s1, s2] = mco(Screamer{}, Screamer{});

  return 0;
}
2 Answers

Since you have a direct one argument, one function mapping, all you really need is std::apply:

template <class... Args>
auto operator()(Args&&... args) {
    return std::apply([&](Fs&... fs){
        return std::tuple(fs(std::forward<Args>(args))...);
    }, fs);
}

This will decay all of the types (that is, if some function/argument pair actually returns an int&, this will give you an int in that spot instead). This also isn't SFINAE-friendly.

A solution that is both SFINAE-friendly and maintains references just needs an extra dash of fold-expression:

template <class... Args,
    std::enable_if_t<(std::is_invocable_v<Fs&, Args> && ...), int> = 0>
auto operator()(Args&&... args) {
    return std::apply([&](Fs&... fs){
        return std::tuple<std::invoke_result_t<Fs&, Args>...>(
            fs(std::forward<Args>(args))...);
    }, fs);
}

You have to use auto f() -> decltype(RET) { return RET; } way (Macro can be used to avoid the boiler plate :-/ and we can also handle noexcept).

Something like:

template <class... Fs>
struct merge_call_object {
private:
  std::tuple<Fs...> fs;
private:

    template <std::size_t... Is, typename Tuple>
    auto call(std::index_sequence<Is...>, Tuple&& tuple)
    -> decltype(std::make_tuple(std::get<Is>(fs)(std::get<Is>(std::forward<Tuple>(tuple)))...))
    {
        return std::make_tuple(std::get<Is>(fs)(std::get<Is>(std::forward<Tuple>(tuple)))...);
    }      
public:
  merge_call_object(Fs... _fs)
      : fs(std::move(_fs)...) {} 

  template <class... Args>
  auto operator()(Args &&... args)
  -> decltype(call(std::index_sequence_for<Fs...>(), std::forward_as_tuple(std::forward<Args>(args)...)))
  {
    return call(std::index_sequence_for<Fs...>(), std::forward_as_tuple(std::forward<Args>(args)...));
  }

};

Demo

Related