Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Deducing Multiple Parameter Packs

Background

I'm trying to write some template functions for a template-only unit test library, specifically for Qt.

Problem

In this library, I have a variadic template that receives a variable amount of objects and functors (Qt5 Signals actually), always paired next to each other, as in QObject, signal, etc... then desirably followed by a variable amount of signal arguments.

Desired Solution

// implementation.h

template <typename T, typename U, typename... Sargs, typename... Fargs>
void test_signal_daisy_chain(T* t,  void(T::*t_signal)(Fargs...), 
                             U* u,  void(U::*u_signal)(Fargs...), 
                             Sargs... sargs, 
                             Fargs... fargs) {...}

// client.cpp

test_signal_daisy_chain(object, &Object::signal1, 
                        object, &Object::signal2, 
                        object, &Object::signal3, 
                        1, 2, 3); // where the signals are defined as void(Object::*)(int, int, int)

Where Fargs... corresponds to both the parameters in t_signal and u_signal as well as the arguments to pass to this function for testing, and Sargs... corresponds to a variable amount of QObject and signal member functions (void(T::*)(Fargs...)) to emit for the express purpose of testing.

Unsurprisingly I get "no matching function" due to "template argument deduction/substitution failed", and my ClangCodeModel plugin warns me that 6 arguments were expected, where 8 were given.

Working (ugly) solution

// implementation.h
template <typename... Fargs>
struct wrapper
{
    template <typename T, typename U, typename... Sargs>
    void test_signal_daisy_chain(Fargs... fargs, 
                                 T* t,  void(T::*t_signal)(Fargs...), 
                                 U* u,  void(U::*u_signal)(Fargs...), 
                                 Sargs... sargs) {...}

// client.cpp

wrapper<int, int, int>::test_signal_daisy_chain(1, 2, 3, 
                                                object, &Object::signal1,
                                                object, &Object::signal2,
                                                object, &Object::signal3);

I'm not content with having to explicitly define the variable function arguments at both the beginning of the function call and in the wrapper template type parameters. In fact, I was initially surprised that the could not be deduced simply by the fact that they were to match the variable arguments of the functors. I'm open to using wrapper functions as opposed to wrapper classes, as I already have a detail namespace set up which I'm willing to get messy for in order to provide a clean and user-friendly API.

Note: signal arguments can be anywhere from primitives to user-defined types to POD structs to template classes, all of variable length.

Edit 1: c++11 is a hard requirement so you can leave >c++11 features in your answer as long as they have some c++11 workaround, i.e. auto... is easy to fix, auto myFunction = []() constexpr {...}; much less so. If using if constexpr instead of a recursive template <std::size_t> helper function saves space and provides for a more succinct, complete, and future-proof answer, then please opt for whichever standard you deem best.

like image 989
jfh Avatar asked Sep 04 '18 14:09

jfh


2 Answers

The simplest approach is to pack the parameters into a tuple at the beginning, and pass the tuple to test_signal_daisy_chain_impl:

template < typename... Fargs, 
          typename T, typename... Sargs>
void test_signal_daisy_chain_impl(const std::tuple<Fargs...> & fargs, 
                                  T* t, void(T::*t_signal)(Fargs...),
                                  Sargs &&... sargs)
{
    // apply unpacks the tuple
    std::apply([&](auto ...params) 
               {
                   (t->*t_signal)(params...);
               }, fargs);

    // Although packed into the tuple, the elements in
    // the tuple were not removed from the parameter list,
    // so we have to ignore a tail of the size of Fargs.
    if constexpr (sizeof...(Sargs) > sizeof...(Fargs))
       test_signal_daisy_chain_impl(fargs, std::forward<Sargs>(sargs)...);
}

// Get a tuple out of the last I parameters
template <std::size_t I, typename Ret, typename T, typename... Qargs>
Ret get_last_n(T && t, Qargs && ...qargs)
{
    static_assert(I <= sizeof...(Qargs) + 1, 
                  "Not enough parameters to pass to the signal function");
    if constexpr(sizeof...(Qargs)+1  == I)
       return {std::forward<T>(t), std::forward<Qargs>(qargs)...};
    else
       return get_last_n<I, Ret>(std::forward<Qargs>(qargs)...);
}    

template <typename T, typename... Fargs, 
          typename... Qargs>
void test_signal_daisy_chain(T* t, void(T::*t_signal)(Fargs...),
                             Qargs&&... qargs)
{
    static_assert((sizeof...(Qargs) - sizeof...(Fargs)) % 2 == 0,
                  "Expecting even number of parameters for object-signal pairs");
    if constexpr ((sizeof...(Qargs) - sizeof...(Fargs)) % 2 == 0) {
        auto fargs = get_last_n<sizeof...(Fargs), std::tuple<Fargs...>>(
                                 std::forward<Qargs>(qargs)...);
        test_signal_daisy_chain_impl(fargs, t, t_signal, 
                                     std::forward<Qargs>(qargs)...);
    }
}

And the usage:

class Object {
public:
    void print_vec(const std::vector<int> & vec)
    {
        for (auto elem: vec) std::cout << elem << ", ";
    }
    void signal1(const std::vector<int> & vec) 
    { 
        std::cout << "signal1(";
        print_vec(vec);
        std::cout << ")\n";
    }
    void signal2(const std::vector<int> & vec) 
    { 
        std::cout << "signal2(";
        print_vec(vec);
        std::cout << ")\n";
    }
    void signal_int1(int a, int b) 
    { std::cout << "signal_int1(" << a << ", " << b << ")\n"; }
    void signal_int2(int a, int b) 
    { std::cout << "signal_int2(" << a << ", " << b << ")\n"; }
    void signal_int3(int a, int b) 
    { std::cout << "signal_int3(" << a << ", " << b << ")\n"; }
};

int main()
{
   Object object;
   test_signal_daisy_chain(&object, &Object::signal1,
                           &object, &Object::signal2 ,
                           std::vector{1,2,3});
   test_signal_daisy_chain(&object, &Object::signal_int1,
                           &object, &Object::signal_int2 ,
                           &object, &Object::signal_int3,
                           1,2);
}

Edit 1

Since C++11 is a hard constraint, there is a much uglier solution, based on the same principles. Things like std::apply and std::make_index_sequence have to be implemented. Overloading is used instead of if constexpr(....) :

template <std::size_t ...I>
struct indexes
{
    using type = indexes;
};

template<std::size_t N, std::size_t ...I>
struct make_indexes
{
    using type_aux = typename std::conditional<
                    (N == sizeof...(I)),
                    indexes<I...>,
                    make_indexes<N, I..., sizeof...(I)>>::type;
    using type = typename type_aux::type;
};

template <typename Tuple, typename T, typename Method, std::size_t... I>
void apply_method_impl(
    Method t_signal, T* t, const Tuple& tup, indexes<I...>)
{
    return (t->*t_signal)(std::get<I>(tup)...);
}

template <typename Tuple, typename T, typename Method>
void apply_method(const Tuple & tup, T* t, Method t_signal)
{
      apply_method_impl(
        t_signal, t, tup,
        typename make_indexes<
             std::tuple_size<Tuple>::value>::type{});
}

template < typename... Fargs,  typename... Sargs>
typename std::enable_if<(sizeof...(Fargs) == sizeof...(Sargs)), void>::type 
test_signal_daisy_chain_impl(const std::tuple<Fargs...> & , 
                             Sargs &&...)
{}

template < typename... Fargs, 
          typename T, typename... Sargs>
void test_signal_daisy_chain_impl(const std::tuple<Fargs...> & fargs, 
                                  T* t, void(T::*t_signal)(Fargs...),
                                  Sargs &&... sargs)
{
    apply_method(fargs, t, t_signal);

    // Although packed into the tuple, the elements in
    // the tuple were not removed from the parameter list,
    // so we have to ignore a tail of the size of Fargs.
    test_signal_daisy_chain_impl(fargs, std::forward<Sargs>(sargs)...);
}

// Get a tuple out of the last I parameters
template <std::size_t I, typename Ret, typename T, typename... Qargs>
typename std::enable_if<sizeof...(Qargs)+1  == I, Ret>::type
get_last_n(T && t, Qargs && ...qargs)
{
    return Ret{std::forward<T>(t), std::forward<Qargs>(qargs)...};
}    

template <std::size_t I, typename Ret, typename T, typename... Qargs>
typename std::enable_if<sizeof...(Qargs)+1  != I, Ret>::type
get_last_n(T && , Qargs && ...qargs)
{
    static_assert(I <= sizeof...(Qargs) + 1, "Not enough parameters to pass to the singal function");
    return get_last_n<I, Ret>(std::forward<Qargs>(qargs)...);
}    

template <typename T, typename... Fargs, 
          typename... Qargs>
void test_signal_daisy_chain(T* t, void(T::*t_signal)(Fargs...),
                             Qargs&&... qargs)
{
    static_assert((sizeof...(Qargs) - sizeof...(Fargs)) % 2 == 0,
                  "Expecting even number of parameters for object-signal pairs");
    auto fargs = get_last_n<sizeof...(Fargs), std::tuple<Fargs...>>(
                             std::forward<Qargs>(qargs)...);
    test_signal_daisy_chain_impl(fargs, t, t_signal, 
                                     std::forward<Qargs>(qargs)...);
}

Edit 2

It is possible to avoid runtime recursion by storing all parameters in a tuple. The following test_signal_daisy_chain_flat() does exactly that, while retaining the same interface as test_signal_daisy_chain():

template <typename Fargs, typename Pairs, std::size_t ...I>
void apply_pairs(Fargs && fargs, Pairs && pairs, const indexes<I...> &)
{
    int dummy[] = {
        (apply_method(std::forward<Fargs>(fargs),
                      std::get<I*2>(pairs),
                      std::get<I*2+1>(pairs)),
         0)...
    };
    (void)dummy;
}
template <typename T, typename... Fargs, 
          typename... Qargs>
void test_signal_daisy_chain_flat(T* t, void(T::*t_signal)(Fargs...),
                                  Qargs&&... qargs)
{
    static_assert((sizeof...(Qargs) - sizeof...(Fargs)) % 2 == 0,
                  "Expecting even number of parameters for object-signal pairs");
    auto fargs = get_last_n<sizeof...(Fargs), std::tuple<Fargs...>>(
                             std::forward<Qargs>(qargs)...);
    std::tuple<T*, void(T::*)(Fargs...), const Qargs&...> pairs{
        t, t_signal, qargs...};
    apply_pairs(fargs, pairs,
                typename make_indexes<(sizeof...(Qargs) - sizeof...(Fargs))/2>
                ::type{});
}

Caveats:

  1. Not asserting that parameter pairs match. The compiler simply fails to compile (possibly deep in recursion).
  2. The types of the parameters passed to the function are deduced from the signature of the first function, regardless of the types of the trailing parameters - the trailing parameters are converted to the required types.
  3. All functions are required to have the same signature.
like image 168
Michael Veksler Avatar answered Nov 10 '22 23:11

Michael Veksler


template<class T>
struct tag_t { using type=T; };
template<class Tag>
using type_t = typename Tag::type;

template<class T>
using no_deduction = type_t<tag_t<T>>;

template <typename T, typename U, typename... Sargs, typename... Fargs>
void test_signal_daisy_chain(
  T* t, void(T::*t_signal)(Sargs...),
  U* u, void(U::*u_signal)(Fargs...),
  no_deduction<Sargs>... sargs,
  no_deduction<Fargs>... fargs)

I assume the Fargs... in t_signal was a typo, and was supposed to be Sargs.

If not, you are in trouble. There is no rule that "earlier deduction beats later deduction".

One thing you can do in c++14 is to have a function returning a function object:

template <typename T, typename U, typename... Fargs>
auto test_signal_daisy_chain(
  T* t, void(T::*t_signal)(Fargs...),
  U* u, void(U::*u_signal)(Fargs...),
  no_deduction<Fargs>... fargs
) {
  return [=](auto...sargs) {
    // ...
  };
}

Then use looks like:

A a; B b;
test_signal_daisy_chain( &a, &A::foo, &b, &B::bar, 1 )('a', 'b', 'c');

doing this in c++11 is possible with a manually written function object.

like image 42
Yakk - Adam Nevraumont Avatar answered Nov 10 '22 23:11

Yakk - Adam Nevraumont