variadic template of a specific type

Backstroy

When trying to achieve more or less what the OP tried to do, I stumbled across this rather old question. I had implemented something similar to @Kerrek SB 's solution, and was looking for a way to 'generalize' this behavior (by providing a predicate as templated struct which is evaluated for not having to re-implement the 'recursion' for different predicates). However while doing so, I realized that the new CPP20 Concepts feature solves this issue in a very elegant way, and hence want to share this solution. I hadn't considered concepts as solution, as I read somewhere that they cannot be declared in a recursive way (which, as I found out, is not a practical burden, because I can refer to a recursive type trait solution).

Solution

In this solution I define a custom concept, because I consider most real-world use-cases to not be covered by concepts provided by the STL, which could be used in this case. Simply replacing the 'typename' by the concept in use, the concept is applied to all of the provided template arguments.

#include <concepts>
#include <type_traits>

template <typename T> concept unsignedType = std::is_unsigned_v<T>; 

struct Array {
  template <unsignedType... Sizes> Array(Sizes... sizes) {
    unsigned args[] = {sizes...};
  }
};

int main() {
  unsigned k = 12;
  Array arr(k, 1u); 
  //Array arr(33);  // fails as 33 is not unsigned
}

Compilation

I am using gcc-10, in previous versions concept features might not be available as this feature is experimental: g++-10.0 -std=c++2a file.cc


I'm not sure why you expected that to work. Clang tells me the error is unknown type name 'Sizes' in the declaration of the constructor. Which is to be expected, since Sizes isn't a type (or rather, a template pack of types), it's a template pack of values.

It's unclear what exactly you're trying to do here. If you pass integral values in as template parameters, what are the constructor parameters supposed to be?


Update: With your new code all you need is a static_cast<unsigned>().

struct Array
{
    template <typename... Sizes> // this works
    Array(Sizes... sizes)
    {
        unsigned args[] = { static_cast<unsigned>(sizes)... };
        // ...snipped...
    }
};

If you want to accept dynamic arguments that must all be integers, you want an ordinary typename template, but check that all the types are (convertible to) unsigned integers:

#include <type_traits>

struct Array
{
    template <typename ...Args>
    explicit Array(Args ...args,
        typename std::enable_if<all_int<Args...>::value>::type * = nullptr);

    // ...
};

Now you just need the trait:

template <typename...> struct all_int;

template <> struct all_int<> : std::true_type { };

template <typename T, typename ...Rest> struct all_int<T, Rest...>
: std::integral_constant<bool,
       std::is_convertible<T, unsigned int>::value && all_int<Rest>::value>
{ }

If you prefer to make the types strict, you can also use is_same instead of is_convertible.

Another option is to forgo variadic templates entirely and make your class list-initializable by accepting a single std::initializer_list<unsigned int>, which provides considerably better numeric safety (for instance, narrowing conversions are forbidden).


Look into initializer list

You could specify it like

struct Array
{
    Array(std::initializer_list<unsigned> sizes)
    {
        for (auto i = sizes.begin(); i != sizes.end(); ++i)
            ...
    }
}

Although, usage would change to

Array arr = {1, 1};