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) { }
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.
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.)
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? Explanation: Template parameters are valid inside a block only i.e. they have block scope.
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!
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 int
s, 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)
{
// ...
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With