ISO/IEC JTC1 SC22 WG21 N4072 — 2014-05-26
M. Bos <m-ou.se@m-ou.se>
This document proposes an extension to the T...
-notation for parameter packs: T...[N]
to give the pack a fixed size. (This might sound close to useless at first, but this document should hopefully convince you otherwise.) This allows for elegant and readable solutions for problems that used to require a lot of confusing boiler plate code, such as taking a variable number of parameters of the same type.
In both template parameter lists and function parameter lists, T...[N] p
would make p
a parameter pack of exactly N
T
s. N
must be a constant expression.
So, void foo(int...[3] args)
would make args
a pack of three integers, such that foo
can be called as foo(1,2,3)
. Similarly, template<typename...[2] Args> void foo(Args... args)
would make Args
a pack of two typenames (and therefore args
a pack of two objects of those two types).
This section describes a few use cases. All first describe the scenario, then give a solution with what's currently possible with C++14, and then give a solution using the proposed idea.
The first two/three are the most important and interesting use cases.
Imagine we're making a class my_vector3
, which represents a three-dimensional mathematical vector of integers. It has a constructor my_vector3(int, int, int)
that takes exactly three integers, as you'd probably expect of such a class:
struct my_vector3 {
// ...
my_vector3(int x, int y, int z)
: values{x, y, z} {}
};
Now we want to turn this into a class template my_vector<N>
, such that the number of dimensions is not fixed to three. Logically, it should have a constructor that takes exactly N
integers, but how would we do that?
This is probably the closest we can get with C++14:
// Helper template gen_tuple:
// gen_tuple<3, int>::type becomes std::tuple<int, int, int>, etc.
template<unsigned int N, typename T> struct gen_tuple {
using type = decltype(
std::tuple_cat(
std::declval<std::tuple<T>>(),
std::declval<typename gen_tuple<N-1, T>::type>()
)
);
};
template<typename T> struct gen_tuple<0, T> {
using type = std::tuple<>;
};
namespace impl {
// This template will only ever be instantiated with an std::tuple of ints.
template<typename> struct my_vector;
template<typename... ints> struct my_vector<std::tuple<ints...>> {
static constexpr unsigned int N = sizeof...(ints);
// ...
my_vector(ints... v) : values{v...} {}
};
}
template<unsigned int N>
using my_vector = impl::my_vector<typename gen_tuple<N, int>::type>;
Not only does this take a lot of code, defining impl::my_vector
this way is confusing and error prone. (Also, note that using this solution, N
can not be inferred when calling something like template<unsigned int N> void foo(my_vector<N>)
. However, this can be fixed by changing the alias template for a class template that has impl::my_vector
as a base and inherits the constructors.)
With the 'fixed size parameter packs' feature this document proposes, the my_vector
template can simply be written as:
template<unsigned int N>
struct my_vector {
// ...
my_vector(int...[N] v) : values{v...} {}
};
Beautiful!
Let's say we have a function void foo3(int, int, int)
, that does something with three integers. However, instead of also writing a foo4
to do it with four integers, we'd like to turn it into a template such that we have foo<N>
for any N
.
template<bool...> constexpr bool all = true;
template<bool head, bool... tail> constexpr bool all<head, tail...> = head && all<tail...>;
template<
typename... T,
typename = std::enable_if_t<
all<std::is_convertible<T, int>::value...>
>
>
void foo(T&&...);
A few notes:
foo3(1, 2u, 3l)
would convert the 2u
and 3l
to int
s before the function call, foo(1, 2u, 3l)
would convert them later, inside the function. (Which doesn't really matter for integral types, but for other types it could be more significant.)T...
: foo(1, 2, 3)
, foo(1u, 2l, 3)
, etc.foo
with wrong parameters.So, this requires some boiler plate code, it is not very readable, and although it comes close, it is not exactly what we wanted.
template<unsigned int N> void foo(int...[N] v);
Beautiful!
See Discussion of Use Case 2 for a small discussion about this use case.
Suppose we have a class template iterleaving_iterator<ForwardIterator, N>
which is an iterator that interleaves N
streams from iterators of the type ForwardIterator
. (Such that a interleaving_iterator<vector<int>::iterator, 3>
allows us to iterate over {1,2,3}
, {10,20,30}
, and {100,200,300}
at once as {1,10,100,2,20,200,3,30,300}
.)
Now, we want a function interleave
that takes itererators and returns such an interleaving_iterator
, with both template parameters automatically deduced. (Such that auto it = interleave(a.begin(), b.begin(), c.begin());
works.)
template<typename... T> struct all_the_same {};
template<typename T> struct all_the_same<T> {
using type = T;
};
template<typename Head, typename... Tail> struct all_the_same<Head, Head, Tail...>
: all_the_same<Head, Tail...> {};
template<typename... T>
interleaving_iterator<typename all_the_same<T>::type, sizeof...(T)> interleave(T... it) {
return { it... };
}
template<typename Iterator, unsigned int N>
interleaving_iterator<Iterator, N> interleave(Iterator...[N] it) {
return { it... };
}
Other than that this code is much easier to write and read, the error message a user gets when accidentally calling interleave
with arguments of different types can just explain the user that interleave
expects all it
arguments to be of the same type.
With template<unsigned int N> struct foo
, add a member function foo<N>::bar
taking exactly N parameters of any type.
template<unsigned int N>
struct foo {
template<typename... T, typename = std::enable_if_t<sizeof...(T) == N>> void bar(T... v);
};
template<unsigned int N>
struct foo {
template<typename...[N] T> void bar(T... v);
};
And as a last example, the proposed feature could be used just to avoid repeating parameters:
void foo(int x, int y, int z, int w) { do_magic(x, y, z, w); }
template<typename A, typename B, typename C, typename D, typename E>
void bar(A a, B b, C c, D d, E e);
(Of course, typename...
and enable_if_t
with sizeof...
could be used for bar
, but that was already used in use case 4.)
void foo(int...[4] v) { do_magic(v...); }
template<typename...[5] T>
void bar(T... v);
In C++, it's very easy to make a function that takes
void foo(int, int);
template<typename A, typename B> void foo(A, B);
template<typename... T> void foo(T...);
Unfortunately, option 3 would be useful but is missing. The only option now is to use option 4 combined with enable_if
and is_convertible
. (Imagine a world where option 1 doesn't exist, such that the only way is to use option 2 combined with enable_if
and is_convertible
.)
A suggestion I've often heard as a solution to this problem is this:
void foo(int... a);
(This could seem very logical, since the ...
here is used the same as in template<typename...>
.)
Apart from that the syntax itself is a problem (since void foo(int...)
is already a C vararg
function), there's another problem: This defines a template, not just a function, without using the keyword template
. From recent discussions about whether to allow void foo(auto a)
or not to define a function template, this seems like a bad idea. So, we want to add the template
keyword explicitly:
template</* What do we put here? */> void foo(int... a);
The only difference between the instantiations of foo
is the number of parameters, so it'd make sense to make this the template parameter:
template<unsigned int> void foo(int... a);
But we need to specify that this integer is the length of the pack. To do that, we would end up with something like:
template<unsigned int N> void foo(int...[N] v);
And this is exactly what this paper proposes.
With template<typename T, unsigned int N> void foo(T...[N]) {}
, T
can be deduced for any N > 0
using the same rules as used for template<typename T> void bar(T, T, T) {}
.
More interesingly, it is important that N
can be deduced: N
would be deducable if the parameter list ends with a fixed size parameter pack of size N
. For example, in these cases template argument deduction would work:
template<unsigned int N> void f(int...[N]) {}
f(1,2,3); // N is deduced as 3.
template<unsigned int N> void f(int, int...[N]) {}
f(1,2,3); // N is deduced as 2.
template<unsigned int N, unsigned int M> void f(int...[N], int...[M]) {}
f<2>(1,2,3); // M is deduced as 1. (N can't be deduced, but is given.)
And it would work for none of these cases:
template<unsigned int N> void f(int...[N], int) {}
f(1,2,3);
template<unsigned int N> void f(int...[N], int...[N]) {}
f(1,2,1,2);
template<unsigned int N> void f(int...[N*2]) {}
f(1,2,3,4);
TODO
(I could use some help here.)
I want to thank the people on Freenode's ##C++ and cpp-proposals@isocpp.org I discussed this idea with. Also, thank you, Filip Roséen <filip.roseen@gmail.com>, for your helpful feedback and coming up with the C++14 solution for use case 1.