Fixed Size Parameter Packs

ISO/IEC JTC1 SC22 WG21 N4072 — 2014-05-26

Maurice Bos <m-ou.se@m-ou.se>

Summary

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.

The Proposed Idea

In both template parameter lists and function parameter lists, T...[N] p would make p a parameter pack of exactly N Ts. 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).

Use Cases and Examples

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.

1) A constant (dependent) number of parameters

Scenario

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?

Current solution

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.)

New solution

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!

2) A variable number of parameters of a specific type

Scenario

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.

Current solution

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:

So, this requires some boiler plate code, it is not very readable, and although it comes close, it is not exactly what we wanted.

New solution

template<unsigned int N> void foo(int...[N] v);

Beautiful!

Discussion

See Discussion of Use Case 2 for a small discussion about this use case.

3) A variable number of parameters of the same type

Scenario

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.)

Current solution

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... };
}

New solution

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.

4) A fixed number of parameters (of any mixed types)

Scenario

With template<unsigned int N> struct foo, add a member function foo<N>::bar taking exactly N parameters of any type.

Current solution

template<unsigned int N>
struct foo {
    template<typename... T, typename = std::enable_if_t<sizeof...(T) == N>> void bar(T... v);
};

New solution

template<unsigned int N>
struct foo {
    template<typename...[N] T> void bar(T... v);
};

5) Just to avoid repetition

And as a last example, the proposed feature could be used just to avoid repeating parameters:

Current solution

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.)

New solution

void foo(int...[4] v) { do_magic(v...); }

template<typename...[5] T>
void bar(T... v);

Discussion of Use Case 2

In C++, it's very easy to make a function that takes

  1. two integers: void foo(int, int);
  2. two parameters of any type: template<typename A, typename B> void foo(A, B);
  3. any number of integers.
  4. any number of parameters of any type: 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.

Template Argument Deduction

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);

Technical Specification

TODO

(I could use some help here.)

Acknowledgements

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.

N4072 — Fixed Size Parameter Packs — Maurice Bos <m-ou.se@m-ou.se>