0

Given the following helper functions

template <typename ... Ts>
auto f(Ts&& ... args) {}

template <typename T>
auto g(T x) { return x; }

1) We expand a template parameter pack as usual.

template <typename ... Ts>
void test1(Ts&& ... args)
{
    f(args...);
}

2) Here the expansion ... occurs after the function call of g(). This is also reasonable, since g() is being called with each args:

template <typename ... Ts>
void test2(Ts&& ... args)
{
    f(g(args)...);
}

3) With the same logic I would expect test3(Is, args...)..., but no. You have to write test3(Is..., args...):

template <typename ... Ts>
void test3(size_t i, Ts&& ... args)
{
    f(args...);
}

template <typename ... Ts>
void test3(std::index_sequence<Is...>, Ts&& ... args)
{
    // would expect test3(Is, args...)...;
    test3(Is..., args...);
}

I know it, I use it, but well, I don't get it. The whole concept of template expansion is a form of expression folding. Not in the C++17 way, but in the sense that the subexpression before the ... is being folded (or repeated if you like) in respect to the variadic parameter. In the test3 case we are "folding" the expression test3(Is, args...) in respect to Is. Yet we have to write test3(Is..., args...) instead of test3(Is, args...)....

With this weird logic of the standard you could also write f(g(args...)) instead of f(g(args)...) - however that's invalid. It looks like the language uses different logic in different contexts.

What is the rationale behind the different syntax?

plasmacel
  • 8,183
  • 7
  • 53
  • 101
  • 2
    You cannot chain calls using parameter pack expansion without using a [trick](http://stackoverflow.com/a/17340003/1794345) – Rerito Dec 12 '16 at 16:31
  • @Rerito Thanks, but I know that trick. Unfortunately this doesn't answer the question. – plasmacel Dec 12 '16 at 16:33

2 Answers2

7

In the test3 case we are "folding" the expression test3(Is, args...) in respect to Is. Yet we have to write test3(Is..., args...) instead of test3(Is, args...)....

That's actually incorrect. test3(Is..., args...) will expand Is in place and then args in place. So the call test3(index_sequence<0,1,2>, x, y, z) would end up calling test3(0, 1, 2, x, y, z), which isn't what you want to happen. You want test3(0, x, y, z); test3(1, x, y, z); test3(2, x, y, z);.

The C++17 way of invoking this would be:

(test3(Is, args...), ...);

This isn't really a different syntax. You have two parameter packs that you want to expand differently: args within the function call and Is around it, so that means you have two ...s. The comma is just a way to indicate that these are separate statements.

The freedom in ... placement means you can fold it anyway you need:

(test3(Is, args), ...);    // test3(0,x); test3(1,y); test3(2,z);
(test3(Is..., args), ...); // test3(0,1,2,x); test3(0,1,2,y); test3(0,1,2,z);
test3(Is..., args...);     // test3(0,1,2,x,y,z);

With this weird logic of the standard you could also write f(g(args...)) instead of f(g(args)...) - however that's invalid

That's not weird logic. Those mean different things. The first expands to f(g(a0, a1, a2, ..., aN)) and the second expands to f(g(a0), g(a1), g(a2), ..., g(aN)). Sometimes you need the former and sometimes you need the latter. Having syntax that allows for both is pretty important.

Barry
  • 286,269
  • 29
  • 621
  • 977
0

Here is what your test3 should look like:

template <typename ... Ts>
void test3_impl(size_t i, Ts&&... args) {
    f(std::forward<Ts>(args)...);
}

template <size_t ... Is, typename ... Ts>
void test3(std::index_sequence<Is...>, Ts&&... args) {
    int dummy[] = { 0, (test3_impl(Is, args...), void(), 0)... };      
}

Parameter pack expansion can take place in specific contexts (usually function/template arguments/parameters lists and brace-initializer list). An expansion out of the blue like you did is illegal. To circumvent this, we need to make it happen in a legal context, here an initializer list. However, we must make sure the said initializer list is not ill-formed:

  • It must not be empty: so we throw in a 0 at the beginning
  • It must be well-typed: test3_impl() returns void, so we use the comma operator: (<CALL>, void(), 0). The void() is here to prevent comma operator overloading, it is added to be exhaustive, it is not required in your example.

Finally, that dummy initializer list must be stored somewhere, so an array of int is a good placeholder.

However, when you write:

// Assuming Is... is [I1, IsTail...]
test3_impl(Is..., args...);

This actually calls f(IsTail..., args...) and not f(args...)

Rerito
  • 5,886
  • 21
  • 47