MateuszL
MateuszL

Reputation: 2983

c++ non-type parameter pack expansion

I am writing template function that is parametrized by single type, and has variable number of parameters of the same type (not of different types). It should check if first value is among the rest. I wanted to write it like this:

#include <unordered_set>

template <typename T>
static bool value_in(T val, T vals...) {
    // compiles, but uses only vals[0]:
    const std::unordered_set<T> allowed {vals};
    // error: pack expansion does not contain any unexpanded parameter packs:
    // const std::unordered_set<T> allowed {vals...};
    return allowed.find(val) != allowed.end();
}
// usage
enum class Enumeration {one, two, three};
int main () {
    // should return true -> 0
    return value_in(Enumeration::two,
                    Enumeration::one,
                    Enumeration::two) ? 0 : 1;
}

I expected that second to work, but it doesn't compile because

test.cpp: In function ‘bool value_in(T, T, ...)’:
test.cpp:7:46: error: expansion pattern ‘vals’ contains no argument packs

I see the "(T, T, ...)" instead of "(T, T...)", so probably I messed up function declaration and ended with C-style variadic function.

How to write declaration that will accept arbitrary number of parameters of the same type?

Upvotes: 4

Views: 7952

Answers (2)

max66
max66

Reputation: 66200

First of all, defining a C-style variadic function

static bool value_in (T val, T vals, ...)

the comma before the ... is optional.

So your

static bool value_in(T val, T vals...)

define two not-variadic arguments (val and vals) and an unnamed variadic sequence.

How to write declaration that will accept arbitrary number of parameters of the same type?

There are many ways but, IMHO, with drawbacks

A possible way is the use of SFINAE: you can impose that the variadic types are equal to the first type.

The following is a C++17 possible solution that uses template folding

template <typename T, typename ... Ts>
std::enable_if_t<(std::is_same<T, Ts>::value && ...), bool>
   value_in (T val, Ts ... vals) 
 {
   const std::unordered_set<T> allowed {val, vals ... };

   return allowed.find(val) != allowed.end();
 }

You can develop this solution also in C++11/C++14 but is a little more complicated.

Drawback: the Ts... type are deduced and they must be exactly the same T type.

So if you want, by example, a function that accept a list of std::string(), you can't call it with a char const *

value_in(std::string{"abc"}, "123");

because T, std::string, is different from Ts..., char const *, and SFINAE doesn't enable value_in.

You can use std::is_convertible instead of std::is_same but I suggest another way, in two steps.

First of all you need a custom type traits (with using helper) to select the first type from a list

template <typename T, typename ...>
struct firstType
 { using type = T; };

template <typename T, typename ... Ts>
using firstType_t = typename firstType<T, Ts...>::type;

Now you can write a first step value_in() that intercept all values, detect al types (without restriction) and pass they to a second step function as follows

template <typename T, typename ... Ts>
bool value_in (T val, Ts ... vals) 
 { return value_in_helper<T, Ts...>(val, vals...); }

The second step function change the all Ts... type in T using firstType

template <typename T, typename ... Ts>
bool value_in_helper (T val, firstType_t<T, Ts> ... vals) 
 {
   const std::unordered_set<T> allowed {val, vals ... };

   return allowed.find(val) != allowed.end();
 }

This solution is C++11 compatible.

Drawback: you need a second step.

Advantage (IMHO): this solution pass through a second step function that is declared receiving T types so accept also arguments that are convertible to T.

That is: this solution accept also

value_in(std::string{"abc"}, "123");

because there isn't anymore needs that "123" is exactly a std::string; can also be convertible to std::string.

Upvotes: 5

lubgr
lubgr

Reputation: 38267

I see two options here. You can pass a std::initializer_list, which causes the function signature to change to

#include <initializer_list>

template <typename T>
static bool value_in(T&& val, std::initializer_list<T> vals)
{
    /* Implementation as before */
}

and the calling snippet to

return value_in(Enumeration::two,
      { Enumeration::one, Enumeration::two }) ? 0 : 1;

Note the additional braces here, they are required to construct the initializer list to be passed. A little detail of this approach is the function signature, which immediately reveals that there is only one type to deduce.

If it feels wrong to type the braces, stick with your original attempt and tweak your function such that

template <typename S, typename... T>
static bool value_in(S&& val, T&&... vals) {
   const std::unordered_set<S> allowed {std::forward<T>(vals)...};

   /* As before... */
}

This allows for calling the function as in your original snippet. In contrast to the above solution, this signature obviously has two template parameters, which might require a second look to see that it will fail if S differs from T.

Upvotes: 2

Related Questions