Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C++ parameter pack, constrained to have instances of a single type?

Tags:

Since C++11 we can make template functions which can accept any sequence of arguments:

template <typename... Ts> void func(Ts &&... ts) {    step_one(std::forward<Ts>(ts)...);    step_two(std::forward<Ts>(ts)...); } 

However, suppose that it really only makes sense to call my function in the case where each argument has the same type -- any number of arguments would be okay though.

What's the best way to do that, i.e. is there a good way to constrain the templates to make a nice error message in that case, or ideally, eliminate func from participating in overload resolution when the arguments don't match?


I can make it really concrete if it helps:

Suppose I have some struct:

struct my_struct {   int foo;   double bar;   std::string baz; }; 

Now, I want to be able to do things like, print the members of the struct for debugging purposes, serialize and deserialize the struct, visit the members of the struct in sequence, etc. I have some code to help with that:

template <typename V> void apply_visitor(V && v, my_struct & s) {   std::forward<V>(v)("foo", s.foo);   std::forward<V>(v)("bar", s.bar);   std::forward<V>(v)("baz", s.baz); }  template <typename V> void apply_visitor(V && v, const my_struct & s) {   std::forward<V>(v)("foo", s.foo);   std::forward<V>(v)("bar", s.bar);   std::forward<V>(v)("baz", s.baz); }  template <typename V> void apply_visitor(V && v, my_struct && s) {   std::forward<V>(v)("foo", std::move(s).foo);   std::forward<V>(v)("bar", std::move(s).bar);   std::forward<V>(v)("baz", std::move(s).baz); } 

(It looks a bit laborious to generate code like this, but I made a small library some time ago to help with that.)

So, now I would like to extend it so that it can visit two instances of my_struct at the same time. The use of that is, what if I want to implement equality or comparison operations. In boost::variant documentation they call that "binary visitation" as contrasted with "unary visitation".

Probably, no one will want to do more than binary visitation. But suppose I want to do like, general n-ary visitation. Then, it looks like this I guess

template <typename V, typename ... Ss> void apply_visitor(V && v, Ss && ... ss) {   std::forward<V>(v)("foo", (std::forward<Ss>(ss).foo)...);   std::forward<V>(v)("bar", (std::forward<Ss>(ss).bar)...);   std::forward<V>(v)("baz", (std::forward<Ss>(ss).baz)...); } 

But now, it's getting a little more squirrelly -- if someone passes a series of types that aren't even the same structure type at all, the code may still compile and do something totally unexpected by the user.

I thought about doing it like this:

template <typename V, typename ... Ss> void apply_visitor(V && v, Ss && ... ss) {   auto foo_ptr = &my_struct::foo;   std::forward<V>(v)("foo", (std::forward<Ss>(ss).*foo_ptr)...);   auto bar_ptr = &my_struct::bar;   std::forward<V>(v)("bar", (std::forward<Ss>(ss).*bar_ptr)...);   auto baz_ptr = &my_struct::baz;   std::forward<V>(v)("baz", (std::forward<Ss>(ss).*baz_ptr)...); } 

That at least will cause a compile error if they use it with mismatching types. But, it's also happening too late -- it's happening after the template types are resolved, and after overload resolution I guess.

I thought about using SFINAE, like, instead of returning void, using std::enable_if_t and checking some expression std::is_same<std::remove_cv_t<std::remove_reference_t<...>> for each type in the parameter pack.

But for one, that SFINAE expression is pretty complicated, and for two, it also has a drawback -- suppose someone has a derived class struct my_other_struct : my_struct { ... }, and they want to use it with the visitor mechanism, so some of the parameters are my_struct and some are my_other_struct. Ideally the system would convert all the references to my_struct and apply the visitor that way, and afaik the example I gave above with the member pointers foo_ptr, bar_ptr, baz_ptr would do the right thing there, but it's not even clear to me how to write a constraint like that with SFINAE -- I would have to try to find a common base of all the parameters I guess?

Is there a good way to reconcile those concerns in general?

like image 211
Chris Beck Avatar asked Jul 22 '16 14:07

Chris Beck


People also ask

How do you expand a parameter pack?

Parameter packs can only be expanded in a strictly-defined list of contexts, and operator , is not one of them. In other words, it's not possible to use pack expansion to generate an expression consisting of a series of subexpressions delimited by operator , .

What is a parameter pack?

Parameter packs (C++11) A parameter pack can be a type of parameter for templates. Unlike previous parameters, which can only bind to a single argument, a parameter pack can pack multiple parameters into a single parameter by placing an ellipsis to the left of the parameter name.

What is Variadic template in C++?

Variadic templates are class or function templates, that can take any variable(zero or more) number of arguments. In C++, templates can have a fixed number of parameters only that have to be specified at the time of declaration. However, variadic templates help to overcome this issue.

What is parameter pack in C++?

A function parameter pack is a function parameter that accepts zero or more function arguments. A template with at least one parameter pack is called a variadic template.


1 Answers

With std::common_type, this is straightforward:

template <class... Args, class = std::common_type_t<Args...>> void foo(Args &&... args) {  } 

This will only be guaranteed to be SFINAE-friendly from C++17 onwards, though. Clang and GCC both implement it that way already.

like image 60
Quentin Avatar answered Dec 08 '22 21:12

Quentin