Build template from arguments of functions?

对着背影说爱祢 提交于 2019-12-07 12:29:24

问题


template<class... Foos> // N = sizeof...(Foos)
template<typename... Args> // M = sizeof...(Args)
void split_and_call(Args&&... args)
{
    // Using Python notation here...
    Foos[0](*args[:a]);  // a = arity of Foos[0]
    Foos[1](*args[a:b]); // b-a = arity of Foos[1]
    ...
    Foos[N-1](*args[z:M]); // M-z = arity of Foos[N-1]
}

Assumptions:

  • All types in Foos are callable
  • All types in Foos are unambiguous
  • Any type in Foos may have an arity of 0
  • Args is the concatenation of all of the argument types used by Foos

Can this be done with just Foos and without Args? I'm actually not sure how to do it even if I did explicitly list them both.


回答1:


I've tried to put together a non-recursive instantiating version, but it involves a few utilities that don't currently exist.

split_and_call

Suppose we have F which takes 2 ints, and G that takes 1 int and arguments 1, 2, 3.

Given F, G, tuple(1, 2, 3), index_sequence<0, 1>, index_sequence<2>, we want to call apply_impl(F{}, tuple(1, 2, 3), index_sequence<0, 1>{}) and apply_impl(G{}, tuple(1, 2, 3), index_sequence<2>{}).

Expanding the F, G is simple with Fns{}..., and making the tuple of arguments is also simple with std::forward_as_tuple(std::forward<Args>(args)...). We're left to construct the index_sequences.

Suppose our function arities are [2, 1, 3], we first get the partial sum of this and prepend a 0: [0, 2, 3, 6]. The index ranges we want are: [0, 2), [2, 3), [3, 6).

We split [0, 2, 3, 6] into is = [0, 2, 3], and js = [2, 3, 6] and zip them to get the ranges we want.

template <typename... Fns, typename Args, std::size_t... Is, std::size_t... Js>
void split_and_call_impl(Args &&args,
                         std::index_sequence<Is...>,
                         std::index_sequence<Js...>) {
  int dummy[] = {
      (apply_impl(Fns{}, std::forward<Args>(args), make_index_range<Is, Js>{}),
       0)...};
  (void)dummy;
}

template <typename... Fns, typename... Args>
void split_and_call(Args &&... args) {
  auto partial_sums = partial_sum_t<0, function_arity<Fns>{}...>{};
  auto is = slice<0, sizeof...(Fns)>(partial_sums);
  auto js = slice<1, sizeof...(Fns) + 1>(partial_sums);
  split_and_call_impl<Fns...>(
      std::forward_as_tuple(std::forward<Args>(args)...), is, js);
}

Utilities

  • std::apply (C++17)
  • function_arity
  • make_index_range
  • slice
  • partial_sum

std::apply

The part we need is actually the apply_impl part.

template <typename Fn, typename Tuple, size_t... Is>
decltype(auto) apply_impl(Fn &&fn, Tuple &&tuple, std::index_sequence<Is...>) {
  return std::forward<Fn>(fn)(std::get<Is>(std::forward<Tuple>(tuple))...);
}

function_arity

Used to determine the arity of a function.

template <typename F>
struct function_arity;

template <typename R, typename... Args>
struct function_arity<R (Args...)>
    : std::integral_constant<std::size_t, sizeof...(Args)> {};

template <typename R, typename... Args>
struct function_arity<R (*)(Args...)> : function_arity<R (Args...)> {};

template <typename R, typename... Args>
struct function_arity<R (&)(Args...)> : function_arity<R (Args...)> {};

template <typename R, typename C, typename... Args>
struct function_arity<R (C::*)(Args...) const> : function_arity<R (Args...)> {};

template <typename R, typename C, typename... Args>
struct function_arity<R (C::*)(Args...)> : function_arity<R (Args...)> {};

template <typename C>
struct function_arity : function_arity<decltype(&C::operator())> {};

make_index_range

A variation on make_index_sequence<N> which constructs index_sequence<0, .. N>. make_index_range<B, E> constructs index_sequence<B, .. E>.

template <typename T, typename U, T Begin>
struct make_integer_range_impl;

template <typename T, T... Ints, T Begin>
struct make_integer_range_impl<T, std::integer_sequence<T, Ints...>, Begin> {
  using type = std::integer_sequence<T, Begin + Ints...>;
};

template <class T, T Begin, T End>
using make_integer_range =
    typename make_integer_range_impl<T,
                                     std::make_integer_sequence<T, End - Begin>,
                                     Begin>::type;

template <std::size_t Begin, std::size_t End>
using make_index_range = make_integer_range<std::size_t, Begin, End>;

slice

Slices an index_sequence in the range [Begin, End).

e.g. slice<0, 2>(index_sequence<2, 3, 4, 5>{}) == index_sequence<2, 3>

template <std::size_t... Is, std::size_t... Js>
constexpr decltype(auto) slice_impl(std::index_sequence<Is...>,
                                    std::index_sequence<Js...>) {
  using array_t = std::array<std::size_t, sizeof...(Is)>;
  return std::index_sequence<std::get<Js>(array_t{{Is...}})...>();
}

template <std::size_t Begin, std::size_t End, std::size_t... Is>
constexpr decltype(auto) slice(std::index_sequence<Is...> is) {
  return slice_impl(is, make_index_range<Begin, End>());
}

partial_sum

Functional version of std::partial_sum.

e.g. partial_sum<2, 3, 4> == index_sequence<2, 5, 9>

template <std::size_t... Is>
struct partial_sum;

template <std::size_t... Is>
using partial_sum_t = typename partial_sum<Is...>::type;

template <>
struct partial_sum<> { using type = std::index_sequence<>; };

template <std::size_t I, std::size_t... Is>
struct partial_sum<I, Is...> {

  template <typename Js>
  struct impl;

  template <std::size_t... Js>
  struct impl<std::index_sequence<Js...>> {
    using type = std::index_sequence<I, Js + I...>;
  };

  using type = typename impl<partial_sum_t<Is...>>::type;
};

Full solution on Ideone

Bonus

I'll share this part since I played with this further for fun. I won't go into too much detail since it's not what was asked.

  • Updated the syntax to call(fs...)(args...); so that top-level functions for example can be passed. e.g. call(f, g)(1, 2, 3)
  • Returned the results of each of the function calls as a std::tuple. e.g. auto result = call(f, g)(1, 2, 3)

Full solution on Ideone




回答2:


A sketch was given by @T.C. above. Assuming that function pointers are passed, arity can be simply defined as

template <typename T>
struct arity : arity<std::remove_pointer_t<std::decay_t<T>>> {};
template <typename R, typename... Args>
struct arity<R(Args...)> : std::integral_constant<std::size_t, sizeof...(Args)> {};

The recursive split is then implemented, in C++14, along the lines of

template <std::size_t FI, std::size_t AI, typename... F, typename ArgTuple, std::size_t...indices>
constexpr auto invoke( std::index_sequence<indices...>, std::tuple<F...> const& f, ArgTuple const& args )
  -> std::enable_if_t<FI == sizeof...(F)-1> {
    std::get<FI>(f)(std::get<AI+indices>(args)...);
}
template <std::size_t FI, std::size_t AI, typename... F, typename ArgTuple, std::size_t...indices>
constexpr auto invoke( std::index_sequence<indices...>, std::tuple<F...> const& f, ArgTuple const& args )
  -> std::enable_if_t<FI != sizeof...(F)-1> {
    std::get<FI>(f)(std::get<AI+indices>(args)...);
    invoke<FI+1, AI+sizeof...(indices)>(std::make_index_sequence<arity<std::tuple_element_t<FI+1, std::tuple<F...>>>{}>{}, f, args);
}
template <typename F1, typename... F, typename... Args>
constexpr void invoke( std::tuple<F1, F...> const& f, Args&&... args ) {
    invoke<0, 0>(std::make_index_sequence<arity<F1>{}>{},
                 f, std::forward_as_tuple(std::forward<Args>(args)...));
}

(Bad naming, but whatever). Demo.




回答3:


Sure! Although of course, default arguments won't work.

Approach the problem as one of recursive list processing. The simplest algorithm is to peel the Args and Foos typelist while repeating one step:

  • If the next Foos can be called with the current set of arguments, then call it. Proceed with the next entry in Foos and the current list of Args.
  • Otherwise, add the next entry in Args to the current set of arguments.

Keep everything packaged in tuples for convenience. The best practice is to obtain a tuple of references by std::forward_as_tuple. By passing around complete tuples, you don't need to "explicitly list" either of them, as you mentioned.

/*  Entry point: initialize the function and argument counters to <0, 0>. */
template< typename foos, typename args > // foos and args are std::tuples
void split_and_call( foos f, args a ) {
    split_and_call_impl< 0, 0 >( 0, std::move( f ), std::move( a ) );
}

// fx = function (foo) index, ax = argument index, cur = current arg list.
template< std::size_t fx, std::size_t ax, typename ... cur,
          typename foos, typename args >
// Use expression SFINAE to cancel this overload if the function cannot be called.
decltype( std::declval< std::tuple_element_t<fx,
    // Be careful to keep std::tuple_element in bounds.
    std::enable_if_t< fx < std::tuple_size< foos >::value, foos
> > >()( std::declval< cur >() ... ) )
split_and_call_impl( int, foos && f, args && a, cur && ... c ) {

    // We verified this call will work, so do it.
    std::get< fx >( f )( std::forward< cur >( c ) ... );

    // Now proceed to the next function.
    split_and_call_impl< fx + 1, ax >( 0, std::move( f ), std::move( a ) );
}

// Similar, but simpler SFINAE. Only use this if there's an unused argument.
// Take "char" instead of "int" to give preference to first overload.
template< std::size_t fx, std::size_t ax, typename ... cur,
          typename foos, typename args >
std::enable_if_t< ax < std::tuple_size< args >::value >
split_and_call_impl( char, foos && f, args && a, cur && ... c ) {

    // Try again with one more argument.
    split_and_call_impl< fx, ax + 1 >( 0, std::move( f ), std::move( a ),
        std::forward< cur >( c ) ..., std::get< ax >( std::move( a ) ) );
}

// Terminating case. Ensure that all args were passed to all functions.
template< std::size_t fx, std::size_t ax, typename foos, typename args >
std::enable_if_t< ax == std::tuple_size< args >::value
               && fx == std::tuple_size< foos >::value >
split_and_call_impl( int, foos && f, args && a ) {}

Live demo.

If you have control over the Foos, then you might consider letting each Foo accept an initial subsequence of arguments, and pass the remainder along to the next Foo as a pack.




回答4:


Here's a fairly short solution, that also stores all the return values of each functor (if any) in a tuple:

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

template <typename F> struct ArgumentSize;

template <typename R, typename... Args>
struct ArgumentSize<R(Args...)> : std::integral_constant<std::size_t, sizeof...(Args)> {};

template <typename R, typename C, typename... Args>
struct ArgumentSize<R(C::*)(Args...) const> : ArgumentSize<R (Args...)> {};

template <typename R, typename C, typename... Args>
struct ArgumentSize<R(C::*)(Args...)> : ArgumentSize<R (Args...)> {};

template <typename C>
struct ArgumentSize : ArgumentSize<decltype(&C::operator())> {};
// etc...

struct NoReturnValue {
    friend std::ostream& operator<< (std::ostream& os, const NoReturnValue&) {
        return os << "[no return value]";
    }
};

template <std::size_t Offset, typename F, typename Tuple, std::size_t... Is>
auto partial_apply_with_offset (F f, const Tuple& tuple, const std::index_sequence<Is...>&,
        std::enable_if_t<!std::is_void<std::result_of_t<F(std::tuple_element_t<Offset+Is, Tuple>...)>>::value>* = nullptr) {
    return f(std::get<Offset+Is>(tuple)...);
}

template <std::size_t Offset, typename F, typename Tuple, std::size_t... Is>
auto partial_apply_with_offset (F f, const Tuple& tuple, const std::index_sequence<Is...>&,
        std::enable_if_t<std::is_void<std::result_of_t<F(std::tuple_element_t<Offset+Is, Tuple>...)>>::value>* = nullptr) {
    f(std::get<Offset+Is>(tuple)...);
    return NoReturnValue();
}

template <std::size_t Offset, typename TupleArgs>
std::tuple<> invoke (const TupleArgs&) {return std::tuple<>();}

template <std::size_t Offset, std::size_t First, std::size_t... Rest, typename TupleArgs, typename F, typename... Fs>
auto invoke (const TupleArgs& tupleArgs, F f, Fs... fs) {
    const auto singleTuple = std::make_tuple (partial_apply_with_offset<Offset> (f, tupleArgs, std::make_index_sequence<First>{}));
    return std::tuple_cat (singleTuple, invoke<Offset + First, Rest...>(tupleArgs, fs...));
}

template <typename... Fs, typename... Args>
auto splitAndCall (const Args&... args) {
    const std::tuple<Args...> tuple(args...);
    return invoke<0, ArgumentSize<Fs>::value...>(tuple, Fs{}...);
}

// Testing
struct F {
    int operator()(int x, char y) const { std::cout << "F(" << x << "," << y << ')' << std::endl;   return 0; }
};

struct G {
    void operator()(double x) const { std::cout << "G(" << x << ')' << std::endl; }
};

struct H {
    double operator()() const { std::cout << "H()" << std::endl;   return 3.5; }
};

int main() {
  const std::tuple<int, NoReturnValue, double> t = splitAndCall<F,G,H>(1,'a',3.5);  // F(1,a)   G(3.5)   H()
  std::cout << std::get<0>(t) << ' ' << std::get<1>(t) << ' ' << std::get<2>(t) << '\n';  // 0 [no return value] 3.5
}


来源:https://stackoverflow.com/questions/32288778/build-template-from-arguments-of-functions

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!