4

I am trying to understand the following code:

template <class...Ts, class...Us> void f(void) {};

int main() {
  f<int, char, float, double>();
}

I don't know how template argument deduction deduces Ts and Us. In this answer, I learned that parameter packs are greedy, so Ts will match all specified arguments: [ Ts = int, char, float, double ]. But what about Us: it has not been deduced yet? or what happens in this case?

I expect that the compiler throws a deduction error since it cannot figure out what Us is expanded to.

Can I somehow, in the template argument-list, tell the compiler that I need Ts to be int, char, and Us to be float, double? how I can do that? I mean, can the call expression be something like that:

f<{int, char}, {float, double}>();

I found out that when I edit the above code to be as follows, this will fit my needs:

template <class...> struct S{};
template <class...Ts, class...Us> void g(S<Ts...>, S<Us...>)
{
};

int main() {

  S<int, char> s1;
  S<float, double> s2;

  g(s1 ,s2);
}

But I still need to understand why in this case Ts is [int, char], and Us is [float, double].

What I think in this case is that: Since parameter packs are greedy, template argument deduction deduces Ts as [S<int, char>, S<float, double>] and Us is left off un-deduced or just empty. Right?

This makes me think that

  1. First, [int, char] gets substituted in the place of Ts... in the first function parameter S<Ts..>, so it gets expanded into S<int, char>. Then [float, double] gets substituted in the place of Us... in the second function parameter S<Us..>, so it gets expanded into S<float, double>.

  2. Second, template argument deduction deduces Ts as [int, char] and Us as [float, double].

Is my understanding correct in this case?

mada
  • 1,646
  • 1
  • 15
  • 2
    *I expect that the compiler throws a deduction error since it cannot figure out what Us is expanded to* - Or you know... it just deduces it as empty – StoryTeller - Unslander Monica Aug 08 '22 at 14:36
  • Related (though not identical): https://stackoverflow.com/questions/73227311/distinguishing-r-and-l-value-references-as-template-arguments – Aconcagua Aug 08 '22 at 14:38
  • With `S` `Ts...` can only be deduced as the parameters of the `S` object, not all of the `S` objects that are passed to the function. That means your *This makes me think that* section explains what is actually happening – NathanOliver Aug 08 '22 at 14:42
  • @NathanOliver - "_That means your This makes me think that section explains what is actually happening_" - So you mean that pack expansion is happening _before_ template argument deduction? – mada Aug 08 '22 at 14:47
  • There is no pack expansion going on. The parameter types are `S` and `S`. When you pass `s1` and `s2` the compiler looks at the parameter and sees that `s1` is a `S` and since `Ts...` is not used anywhere else it can just deduce that `Ts...` must be `[int, char]`. The same thing happens for `s2` and since no errors happen the template gets instantiated and then the concrete function that was generated is called. – NathanOliver Aug 08 '22 at 14:51
  • @NathanOliver - _"... **since Ts... is not used anywhere else** it can just deduce that Ts... must be [int, char]"_ What do you mean by that? – mada Aug 08 '22 at 15:03
  • `Ts...` is only used if the first parameter of the function. That means it only will be deuced as the parameter of the `S` passed to it. It's no longer "greedy" as you've constrained it to only the first parameter of the function. – NathanOliver Aug 08 '22 at 15:05
  • @NathanOliver - Thanks but you say "_There is no pack expansion going on._" Does this isn't a pack expansion `P1 = S, A1 = S --> [ Ts = int, char ];`? – mada Aug 08 '22 at 15:25
  • @John `Ts...` is a pack expansion but not `S` – Jason Aug 08 '22 at 15:26
  • @JasonLiam - That's what you have already said `Ts...` is a pack expansion. Why does he say "_there is no pack expansion going on_"? – mada Aug 08 '22 at 15:29
  • 1
    @John Using `Ts...` like `using type = std::common_type` is pack expansions as it expands `Ts...` into the parameters of `common_type`. `template void foo(S)` is not pack expansion, it is just declaring a function parameter that uses the `Ts...` template parameter. – NathanOliver Aug 08 '22 at 15:36
  • The first example is actually ill-formed because a template parameter after a template parameter pack in a function template must be deducible from the function's parameter list or have a default argument. Compilers should issue a diagnostic. See https://eel.is/c++draft/temp#param-example-7. Bug report for e.g. GCC [here](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=69623). – user17732522 Aug 08 '22 at 15:55

2 Answers2

1

But what about Us: it has not been deduced yet?

The first snippet is ill-formed as per temp.param#14:

A template parameter pack of a function template shall not be followed by another template parameter unless that template parameter can be deduced from the parameter-type-list ([dcl.fct]) of the function template or has a default argument ([temp.deduct]).

// U can be neither deduced from the parameter-type-list nor specified
template<class... T, class... U> void f() { }   // error

(emphasis mine)

Note that MSVC correctly rejects the first example which can be seen here.

There is a gcc bug report submitted as:

The template parameter pack of a function template should be the last template parameter.


What I think in this case is that: Since parameter packs are greedy, template argument deduction deduces Ts as [S<int, char>, S<float, double>] and Us is left off un-deduced or just empty. Right?

No, you're wrong in saying the above quoted statement because in the second snippet, the function template has two function parameters. The type of the first parameter is S<Ts...> and the type of the second parameter is S<Us...>.

This means that Ts and Us will be deduced using/from the template arguments of the passed S</*some args here*/> and S</*some args here*/>.

Now lets apply this to your second example. Since you've passed S<int, char> and S<float, double> as call arguments, the Ts gets deduced to the sequence {int, char} while the Us gets deduced to the sequence {float, double}.


Can I somehow, in the template argument-list, tell the compiler that I need Ts to be int, char, and Us to be float, double? how I can do that? I mean, can the call expression be something like that:

f<{int, char}, {float, double}>();

No, first note that the first snippet is ill-formed.

Jason
  • 36,170
  • 5
  • 26
  • 60
  • `Can I somehow, in the template argument-list, tell the compiler that I need Ts to be int, char, and Us to be float, double? how I can do that?);` Can you add an answer for that? – mada Aug 08 '22 at 15:21
  • @John I've added an answer to that in my updated answer. Check out the updated answer. In particular, it is not possible because `Ts` will consume all the passed template arguments such that `Us` will be deduced to the empty sequence, in your first example. – Jason Aug 08 '22 at 15:25
  • @John The first example is invalid/ill-formed. A gcc bug report is submitted here: [The template parameter pack of a function template should be the last template parameter](https://gcc.gnu.org/bugzilla/show_bug.cgi?id=61953). Note also that msvc correctly rejects the first example. [Demo](https://godbolt.org/z/1q53M4z1P) – Jason Aug 08 '22 at 16:03
1

Regarding the second part of the question (second example).

Template argument deduction deduces parameter packs Ts as [int, char], and Us as [ float, double ] the same way it deduces T if you have the following:

template <class> struct B {};
template <class T> void h(B<T>)

.. and you call h(B<int>()), template argument deduction is applied as follows:

P = B<T>, A = B<int> -> [ T = int ] not [ T = B<int> ]

The same is applied in the second example:

P1 = S<Ts...>, A1 = S<int, char> --> [ Ts = int, char ];
P2 = S<Us...>, A2 = S<float, double> --> [ Us = float, double ];
mada
  • 1,646
  • 1
  • 15