How to pass around parameter packs in C++?
Asked Answered
G

5

10

Consider the following example:

template <class T> class method_traits;
template <class T, class Ret, class... Arg> class method_traits<Ret(T::*)(Arg...)> {
public:
    using type = Arg; // this does not work
};

template <class T> using argument_types = typename method_traits<T>::type;

template <class T> class Node {
    T t;
public:
    Node(Input<argument_types<decltype(&T::process)>>... inputs) { // how do I make this work?
        ...
    }
};

The arguments of the constructor of Node<T> depend on the arguments of the method T::process. So if a type T has a method process of the signature float process(float a, int b) the signature of the constructor of Node<T> should look like this: Node(Input<float> a, Input<int> b).

How do I extract the parameter pack from T::process to use it on the constructor of Node?

Gemmagemmate answered 22/10, 2017 at 15:55 Comment(9)
You need to make Node a variadic template too.Burrill
@n.m. but Node only depends on a single type T and not on multiple typesGemmagemmate
@n.m. Surely it suffices to make Node's constructor a variadic template, rather than the whole class?Collegiate
@ArthurTacca it could be, depending on what Node is storing.Burrill
In my case Node stores the Inputs in a tuple so I guess making the constructor a variadic template would work.Gemmagemmate
You cannot say "my member function will have the same signature as T::somefunction. It's a deficiency in C++ syntax. To overcome it, you need to introduce an artificial parameter pack that doesn't express any real dependency.Burrill
What's Input? Is that important to the question?Uncourtly
Note most of the answers will fail if the member function has cv-qualifiers, has ref-qualifiers, or is C-style variadic, (or in C++17, is noexcept). See the ugly sample definition of std::is_function at en.cppreference.com/w/cpp/types/is_function for a hint on how it would need to be done properly.Marmara
@Uncourtly It is not important to the question. It is just there to show that the arguments are not exactly the same but depend on each other.Gemmagemmate
N
6

Obviously you can't save a list of types in this way

    using type = Arg;

where Arg is a variadic list of types.

But you can save they in a type container and std::tuple can do this works too. So I suggest to modify the method_traits specialization as follows

template <typename T>
struct method_traits;

template <typename T, typename Ret, typename... Args>
struct method_traits<Ret(T::*)(Args...)>
 { using tTypes = std::tuple<Args...>; };

and rewrite argument_types to intercept the std::tuple

template <typename T>
using tTypes = typename method_traits<T>::tTypes;

Now you can use the default template value and partial specialization trick defining node

template <typename T, typename TArgs = tTypes<decltype(&T::process)>>
struct Node;

In this way, instantiating a Node<T> object, you effectively get a Node<T, tTypes<decltype(&T::process)> that is a Node<T, std::tuple<Args...>> with the wanted Args....

So you can simply define the following partial specialization of Node as follows

template <typename T, typename ... Args>
struct Node<T, std::tuple<Args...>>
 {
   T t;

   Node (Input<Args> ... inputs)
    { /* do something */ }
 };

The following is a full working example

#include <tuple>
#include <type_traits>

template <typename T>
struct tWrapper
 { using type = T; };

template <typename T>
using Input = typename tWrapper<T>::type;

template <typename T>
struct method_traits;

template <typename T, typename Ret, typename... Args>
struct method_traits<Ret(T::*)(Args...)>
 { using tTypes = std::tuple<Args...>; };

template <typename T>
using tTypes = typename method_traits<T>::tTypes;

template <typename T, typename TArgs = tTypes<decltype(&T::process)>>
struct Node;

template <typename T, typename ... Args>
struct Node<T, std::tuple<Args...>>
 {
   T t;

   Node (Input<Args> ... inputs)
    { /* do something */ }
 };

struct foo
 {
   float process (float a, int b)
    { return a+b; }
 };

int main ()
 {
   Node<foo> nf(1.0f, 2);
 }

-- EDIT --

As pointed by Julius (and the OP themselves) this solution require an additional template type with a default template value.

In this simplified case isn't a problem but I can imagine circumstances where this additional template argument can't be added (by example: if Node receive a variadic list of template arguments).

In those cases, Julius propose a way that complicate a little the solution but permit to avoid the additional template parameter for Node: to add a template base class, that receive the TArgs arguments, and to works with constructor inheritance.

That is: defining a NodeBase as follows

template <typename, typename>
struct NodeBase;

template <typename T, typename ... Args>
struct NodeBase<T, std::tuple<Args...>>
 {
   T t;

   NodeBase (Input<Args> ...)
    { /* do something */ }
 };

there is no need for an additional template parameter, for Node, that can simply written as

template <typename T>
struct Node
   : public NodeBase<T, tTypes<decltype(&T::process)>>
 { using NodeBase<T, tTypes<decltype(&T::process)>>::NodeBase; };

Julius, following this idea, prepared a solution that (IMHO) is even better and interesting.

Nephralgia answered 22/10, 2017 at 16:52 Comment(4)
nice solution, even though it requires to add a second template parameter to NodeGemmagemmate
@Gemmagemmate - yes; but the second parameter is a default one; so, as you can see in main(), you can instantiate a Node simply as Node<foo>.Nephralgia
Very nice solution. If that default template parameter is not acceptable, one could derive from a base class with constructor inheritance.Lefty
@Lefty - thanks; I prefer the default template type way, but I can imagine circumstances where it's an impracticable solution. In that circumstances, your idea can be useful.Nephralgia
G
3

Using perfect forwarding (live):

template<typename... Args>
Node(Args&&... args) {
    process(std::forward<Args>(args)...);
}
Gruesome answered 22/10, 2017 at 16:19 Comment(1)
Honestly this is the answer, all the answers above are needlessly convoluted.Beloved
L
2

Here is one example in C++11 (thanks to max66's comment) based on max66's great answer. The differences here:

  • no additional template argument for Node (instead, using a base class and constructor inheritance)
  • the desired arguments are obtained with a slightly different style than shown in the question
    • adding overloads for qualified member functions is simple
    • references are not a problem (as far as I can tell; see example below printing 42)

http://coliru.stacked-crooked.com/a/53c23e1e9774490c

#include <iostream>

template<class... Ts> struct Types {};

template<class R, class C, class... Args>
constexpr Types<Args...> get_argtypes_of(R (C::*)(Args...)) {
    return Types<Args...>{};
}

template<class R, class C, class... Args>
constexpr Types<Args...> get_argtypes_of(R (C::*)(Args...) const) {
    return Types<Args...>{};
}

template<class T, class ConstructorArgs>
struct NodeImpl;

template<class T, class Arg0, class... Args>
struct NodeImpl<T, Types<Arg0, Args...>> {
  NodeImpl(Arg0 v0, Args...) {
    v0 = typename std::decay<Arg0>::type(42);
    (void)v0;
  }
};

template<class T>
struct Node
  : NodeImpl<T, decltype(get_argtypes_of(&T::process))>
{
  using ConstructorArgs = decltype(get_argtypes_of(&T::process));
  using NodeImpl<T, ConstructorArgs>::NodeImpl;
};

struct Foo {
    void process(int, char, unsigned) const {}
};

struct Bar {
    void process(double&) {}
};

int main() {
    Node<Foo> foo_node{4, 'c', 8u};

    double reftest = 2.0;
    Node<Bar> bar_node{reftest};
    std::cout << reftest << std::endl;
}
Lefty answered 22/10, 2017 at 17:3 Comment(2)
Also your's is a nice solution; if you want it to works with C++11, it's very simple: avoid to return auto in get_argtypes_of() and explicit that return a Types<Args...>. If you're worried for the repetition of Types<Args...>, you can simply return {};. (continue)Nephralgia
And, considering that you use get_argtypes_of() only in a couple of decltypes() (because you're only interested in returned type, non in returned value) you can also declare it and not implement it (as std::declval()); I mean that it's enough template<class R, class C, class... Args> constexpr auto get_argtypes_of(R (C::*)(Args...));, without the implementation of the function.Nephralgia
C
0

Here is a solution using C++14. (Note: I have only tested it in clang, though):

#include <string>
#include <utility>

struct Foo {
    void process(int, std::string);
};

template <typename T>
struct Input { };

template <std::size_t N, typename T, typename ...Types>
struct Extract_type {
    using type = typename Extract_type<N - 1, Types...>::type;
};

template <typename T, typename ...Types>
struct Extract_type<0, T, Types...> {
    using type = T;
};

template <typename T, std::size_t N, typename R, typename ...Args>
typename Extract_type<N, Args...>::type extract(R (T::*)(Args...));

template <typename T, typename R, typename ...Args>
std::integral_constant<std::size_t, sizeof...(Args)> num_args(R (T::*)(Args...));

template <typename T>
struct Node {
    template <typename ...Args>
    Node(Input<Args>&&... args) 
    : Node(std::make_index_sequence<decltype(num_args<T>(&T::process))::value>{ }, std::forward<Input<Args>>(args)...)
    {}

    template <std::size_t ...Indices>
    Node(std::index_sequence<Indices...>, Input<decltype(extract<T, Indices>(&T::process))>...) {}
};


int main() {
    Node<Foo> b{ Input<int>{ }, Input<std::string>{ } };
}

http://coliru.stacked-crooked.com/a/da7670f80a229931

Chace answered 22/10, 2017 at 17:10 Comment(0)
K
0

How about using private inheritance and CRTP?

#include <tuple>
#include <iostream>

template <typename Method> struct method_traits;

template <typename T, typename Ret, typename... Args>
struct method_traits<Ret(T::*)(Args...)> {
public:
    using parameter_pack = std::tuple<Args...>;
};

template <typename Derived, typename Tuple> struct Base;

template <typename Derived, typename... Ts>
struct Base<Derived, std::tuple<Ts...>> {
    void execute_constructor(Ts&&... ts) { 
        Derived* d = static_cast<Derived*>(this);
        d->t.process(std::forward<Ts>(ts)...);
        d->num = sizeof...(Ts);
    }
    virtual ~Base() = default;
};

template <typename T, typename... Rest>
class Node : Base<Node<T, Rest...>, typename method_traits<decltype(&T::process)>::parameter_pack> {
    T t;
    int num;
public:
    using Base = Base<Node<T, Rest...>, typename method_traits<decltype(&T::process)>::parameter_pack>;
    friend Base;  // So that Base can do whatever it needs to Node<T, Rest...>'s data members.
    template <typename... Ts>
    Node (Ts&&... ts) {
        Base::execute_constructor(std::forward<Ts>(ts)...);
        std::cout << "num = " << num << '\n';
    }
};

struct foo {
    void process(int a, char c, bool b) {
        std::cout << "foo(" << a << ", " << c << ", " << std::boolalpha << b << ") carried out.\n";
    }   
};

int main() {
    Node<foo> n(5, 'a', true);
    std::cin.get();
}

Output:

foo(5, a, true) carried out.
num = 3
Kellene answered 15/11, 2017 at 19:20 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.