Build template from arguments of functions?
Asked Answered
F

4

4
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.

Ferdinandferdinanda answered 29/8, 2015 at 17:21 Comment(20)
This isn't the clearest of questions, but are you trying to split the argument list so you can call one of the Foos functors with each piece ?Granulose
Sorry, it's hard to make a nice example. Yes, I'm trying to pack all of the argument types of the Foos, all of which are functions, into the template Args. Then in the function, split and call them correctly.Ferdinandferdinanda
How do you want to deal with overloaded Foos ?Granulose
I'm assuming that the types are already resolved in the parameter list, so whichever overload is chosen by the compiler is the one used.Ferdinandferdinanda
Could you clarify what you want to pass to each of the Foos...? That is, what's i, j, l? is the m in args[m-1] the same m = sizeof...(Args)?Phatic
Ah, your last edit made it more clear what you want to do. What @Quentin's question regarding overloaded Foos needs to be answered still. Suppose F takes 1 or 2 ints and G takes 0, or 1. Given split_and_call<F, G>(1, 2);, Do you call F(1), G(2), or F(1, 2), G()?Phatic
Pastebin link for the annoying case -- i.e who eats the float ? :)Granulose
@Phatic @Granulose When first templated, the arguments needed by each Foos are all known. In the function call itself, the arguments are packed as described by the function declarations. So I suppose the answer is "whichever the compiler happened to choose."Ferdinandferdinanda
Ok, it sounds like you're ok with disallowing the overload case based on your planned usage? If the answer is "whatever the compiler happens to choose", then you must be ok with the compilation failure due to the ambiguity.Phatic
@Phatic yes, whenever I use this function I will have very explicit functions as parameters with no overload ambiguity.Ferdinandferdinanda
Great. Thanks for the clarification!Phatic
Sketching the solution since I'm on a cellphone: you want to build a pack of integer_sequences, such that each sequence contains the indices for the arguments of the corresponding Foo, and do a call(Foos, Seqs, Args...)..., where call is just Foo(get<index>(Args)...).History
To build the pack of sequences, start from a pack N containing the number of arguments required by the Foos, compute a sequence of partial sums, and add_to_each_sequence_member<PartialSums, make_index_sequence<N>>... is the pack of sequences.History
@History okay, I think I can get to the solution with that. Thanks!Ferdinandferdinanda
This is basically the inverse of tuple_cat. Searching for how that is implemented should give you some ideas.History
@History tuple_cat is a cakewalk compared to this :pGranulose
@Granulose not really, the approach is pretty similar given the constraint that the arity of all the Foos are known.History
@History I'm waiting for the reveal then. Dropping two pack expansions side-by-side looks simple enough, cutting one in two I don't see how.Granulose
@Granulose you'll have to wait for a while then. I won't have access to a computer for a few hours, and I'm not writing this stuff on my cellphone. :PHistory
Little late to the party, but you're passing the Foos as types here... did you mean to pass them as args somehow?Diabetes
P
7

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

Phatic answered 29/8, 2015 at 20:38 Comment(4)
Is this the Michael Park of tuple_element_t fame? :)History
@T.C I don't know about fame, but it is indeed. :)Phatic
Hey again, there's a question I had for function_arity, is there a way to have it match constructors? I'd like to be able to call placement news through this syntax but I can't find a way to match them against a templateFerdinandferdinanda
Do you mean to provide the constructor as an argument similar to how you might provide a member function as an argument and call it...? A member function is applied to an instance of an object. Since a constructor is what's necessary for an instance of an object to exist in the first place, we can't apply a constructor. I think the best bet is to wrap the placement-new call within a lambda and pass that along?Phatic
S
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.

Sanborne answered 29/8, 2015 at 20:30 Comment(0)
D
1

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.

Disenthrall answered 30/8, 2015 at 4:47 Comment(0)
B
1

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
}
Blacklist answered 21/3, 2016 at 16:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.