stack_lexi
stack_lexi

Reputation: 383

Can c++11 parameter packs be used outside templates?

I was wondering if I could have parameter packs consisting of a single, explicitly specified, type. For example, something like this:

#include <iostream>

using namespace std;

void show() { }

template<typename First, typename... Rest>
void show(First f, Rest... rest)
{
    cout << f << endl;
    show(rest...);
}

void foo(int f, int... args) // error
{
    show(f, args...);
}

int main()
{
    foo(1, 2, 3);
}

The problem I'm having is with the definition of foo(). With OS X clang++ version 5 (llvm 3.3svn) I get the error error: type 'int' of function parameter pack does not contain any unexpanded parameter packs.

Of course, I can get it to compile by changing to foo() into a function template:

template<typename... Args>
void foo(int f, Args... args)
{
    show(f, args...);
}

However now foo() will accept int for the first parameter, and anything output streamable for the rest. For example:

struct x { };
ostream& operator<<(ostream& o, x)
{
    o << "x";
    return o;
}

int main()
{
    foo(1, 2, x(), 3); // compiles :(
}

Now, I've seen the accepted solution here which suggests using type traits and std::enable_if, but that's cumbersome. They also suggested using std::array but I think a simple std::initializer_list works just fine and looks cleaner, like so:

void foo_impl(initializer_list<int> ints)
{
    for(int i: ints)
        cout << i << endl;
}

template<typename... Args>
void foo(int f, Args... args)
{
    foo_impl({f, args...});
}

struct x { };
ostream& operator<<(ostream& o, x)
{
    o << "x";
    return o;
}

int main()
{
    foo(1, 2, 3);
    foo(1, 2, x(), 3); // no longer compiles
                       // we also get an error saying no known conversion from 'x' to 'int' :)
}

So that's neat. But the question remains, is this necessary? Is there really not a way to define a non-template function which accepts a parameter pack of specific type? Like this:

void foo(int... args) { }

Upvotes: 38

Views: 15781

Answers (6)

Alex Trotta
Alex Trotta

Reputation: 91

As with Brian's answer, I realize this was originally intended for C++11, but in C++20 this can be solved in a very simple way using concepts:

#include <concepts>

void f(std::integral auto... ints)
{
    // ...
}

std::integral accepts any integral type, so it's a bit more general, if that is acceptable. If not, you can do something like the following:

#include <concepts>

template<class T>
concept exactly_int = std::same_as<int,T>;

void f(exactly_int auto... ints)
{
   // ...
}

To add a bit more explanation to this, the auto is essentially an implicit template, and the name before it is constraining what types are allowed. So in the first example, anything that satisfies std::integral (int,long,unsigned,char, etc.) will be allowed. The second allows only ints, since that is the only type that satisfies the concept that was defined.

There is an even simpler way to do this: Concepts when used as constrains use the type that is being constrained as its first argument, so you can simply write:

#include <concepts>

void f(std::same_as<int> auto... ints)
{
   // ...
}

Upvotes: 9

Sylwester
Sylwester

Reputation: 11

You can't use a template like

<MyType i, MyType ... myTypes>

for function, but you cat use it for class/struct. Just change your function to a static struct member and it's done (sum example):

 template<int i, int ... ints>
 struct A <i, ints...> {
    static int Do() {
        return i + A<ints...>::Do();
    }
 };

This is not all job to do - a boundary struct is also needed. All the example (with Show() method):

#include <iostream>

using namespace std;

template<int ...> // declaration needed
struct A {};

template<int i>   // boundary
struct A <i>{
    static void Show() {
        cout << i << endl;
    }
};

template<int i, int ... ints> // main struct
struct A <i, ints...> {
    static void Show() {
        cout << i << endl;
        A<ints...>::Show();
    }
};

int main() {
    A<5, 45, 2, -100>::Show();
    cout << endl;
    A<-15, -3, 2>::Show();
    return 0;
}

Upvotes: -1

Brian Rodriguez
Brian Rodriguez

Reputation: 4359

I realize this is tagged C++11, but the features of C++17/1z works fantastically here so I figured its solution is worth posting:

template<typename... Ints>
void foo(Ints... xs)
{
    static_assert(std::conjunction<std::is_integral<Ints>...>::value);
    (std::cout << xs << '\n', ...);
}

Upvotes: 1

Sarfaraz Nawaz
Sarfaraz Nawaz

Reputation: 361612

void foo(int... args) {}

No you cannot write that.

But you can have the same effect with this approach:

template<typename ...Ints>
void foo(Ints... ints) 
{
   int args[] { ints... }; //unpack ints here
   //use args
}

With this approach, you can pass all int if you want. If any argument passed to foo is not int or convertible to int, the above code will result in compilation error, as it would be the case with int ...args approach if it were allowed.

You could also use static_assert to ensure all Ints are indeed int if you want that behaviour:

template<typename ...Ints>
void foo(Ints... ints) 
{
   static_assert(is_all_same<int, Ints...>::value, "Arguments must be int.");

   int args[] { ints... }; //unpack ints here
   //use args
}

Now you've to implement is_all_same meta-function which is not difficult to implement.

Alright, this is the basic idea. You can write more sophisticated code with variadic templates and with the help of some utility meta-functions and helper functions.

For lots of work that you can do with variadic arguments, you don't even need to store in args[] array, e.g if you want to print the arguments to std::ostream, then you could just do it as:

struct sink { template<typename ...T> sink(T && ... ) {} };

template<typename ...Ints>
void foo(Ints... ints) 
{
    //some code

     sink { (std::cout << ints)... };
}

Here you create a temporary object of type sink so that you use unpack the template arguments using list-initialization syntax.

Last you could just use std::initializer_list<int> itself:

void foo(initializer_list<int> const & ints) 
{

}

Or std::vector<int> in case if you need vector-like behavior inside foo(). If you use any of these, you have to use {} when calling the function as:

f({1,2,3});

That may not be ideal but I think with the advent of C++11 you will see such code very frequently!

Upvotes: 28

user2249683
user2249683

Reputation:

You might specify the type you want to show:

#include <iostream>

template<typename T>
void show_type() {}

template<typename T, typename... Rest>
void show_type(const T& x, Rest... rest)
{
    std::cout << x << std::endl;
    show_type<T>(rest...);
}

template<typename... Args>
void foo(int x, Args... args)
{
    show_type<int>(x, args...);
}

struct X { };
std::ostream& operator<<(std::ostream& o, X)
{
    o << "x";
    return o;
}


int main()
{
    foo(1, 2, 3);
    foo(1, 2, 3.0); // Implicit conversion
    // or just
    show_type<int>(1, 2, 3);
    foo(1, 2, X()); // Fails to compile
}

Upvotes: 2

xtofl
xtofl

Reputation: 41519

Why the foo_impl workaround, and not just use initialize_list<int> in foo's signature directly? It clarifies that you accept a variable-size argument list of said type.

Upvotes: 3

Related Questions