Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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) { }
like image 616
stack_lexi Avatar asked Dec 05 '13 18:12

stack_lexi


People also ask

What is a non type template parameter?

A template non-type parameter is a template parameter where the type of the parameter is predefined and is substituted for a constexpr value passed in as an argument. A non-type parameter can be any of the following types: An integral type. An enumeration type. A pointer or reference to a class object.

What is a template template parameter?

A template argument for a template template parameter is the name of a class template. When the compiler tries to find a template to match the template template argument, it only considers primary class templates. (A primary template is the template that is being specialized.)

What is the role of parameter in a template?

A template parameter is a special kind of parameter that can be used to pass a type as argument: just like regular function parameters can be used to pass values to a function, template parameters allow to pass also types to a function.

What is the validity of template parameters?

What is the validity of template parameters? Explanation: Template parameters are valid inside a block only i.e. they have block scope.


3 Answers

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!

like image 76
Nawaz Avatar answered Sep 22 '22 10:09

Nawaz


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)
{
   // ...
}
like image 42
Alex Trotta Avatar answered Sep 22 '22 10:09

Alex Trotta


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.

like image 20
xtofl Avatar answered Sep 22 '22 10:09

xtofl