Strange compilation errors when instantiating a variadic function template
Asked Answered
A

1

2

Let's first introduce a helper type that represents a parameter pack:

template<typename... T> struct Pack { };

Now, here's the function with the weird behaviour:

template<typename... TT, typename T>
void f(Pack<TT...>, Pack<T>, std::type_identity_t<TT>..., std::type_identity_t<T>);

std::type_identity_t is used here to disable deduction from the last two parameters in case that would introduce any ambiguity.

First, I tried to call it like so:

f(Pack<int>{}, Pack<int>{}, 5, 5);

GCC raises an error and gives the following explanation:

<source>:12:6: note:   candidate expects 3 arguments, 4 provided
   12 |     f(Pack<int>{}, Pack<int>{}, 5, 5);
      |     ~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

That's not the kind of error I expected, I assumed that deduction would result in T = int and TT... = int. But okay, let's do what the note says and provide one less argument:

f(Pack<int>{}, Pack<int>{}, 5);

It still gives an error, but this time it's:

<source>:12:6: error: too few arguments to function 'void f(Pack<TT ...>, Pack<T>, std::type_identity_t<TT>..., std::type_identity_t<T>) [with TT = {int}; T = int; std::type_identity_t<T> = int]'
   10 |     f(Pack<int>{}, Pack<int>{}, 5);
      |     ~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Now it's too few of them. Also notice that this error message confirms my assumption about the deduced T and TT....

At this point I switched to Clang to see whether it'd compile this code, but both of the above calls to f (with 3 and 4 arguments) result in the same error, with the note:

<source>:8:6: note: candidate template ignored: deduced packs of different lengths for parameter 'TT' (<int> vs. <>)
void f(Pack<TT...>, Pack<T>, std::type_identity_t<TT>..., std::type_identity_t<T>);
     ^

I also tried compiling the code with MSVC, and it did compile without errors.

What is going on? Is there something that makes this code invalid? Is it a compiler bug, due to the strange error messages?

Alphabetic answered 13/6, 2022 at 12:5 Comment(10)
What if you use std::type_identity_t<TT>... to avoid deduction here?Lip
@user253751 I did, as described in the question, it changes none of the errors.Alphabetic
Regarding whereas in my case, TT... is unambiguous, because it's deduced from Pack<TT...>. Templates don't worj that way. All parameters that use TT... must still match what they deduce TT... to be. If they do not match, deduction fails.Fiorenza
@user17732522 I understand what you mean, but I thought that saying 'everything is the same, std::type_identity changes nothing' would suffice. Regarding what @Fiorenza said, I very much doubt that there's a deduction problem with the last two parameters, due to std::type_identity not having any effect.Alphabetic
@user17732522 Okay, I see I should have skipped the case without std::type_identity. I I'll just edit the question.Alphabetic
I believe this is not really a dupe: the dupe is covered (although never mentioned) by [temp.deduct.type]/5.7. std::type_identity makes no difference as the TT...s are in a non-deduced context anyway. The core of this question is why then they cannot be deduced from the Pack<TT...> and simply "inserted and expanded" from that deduction in the non-deduced context of the third function parameter (TT...)..Versed
@Versed Ah right, but is the T a the end still a deduced context? If so that certainly can't be deduced.Sinistrous
@Sinistrous Possibly, although I can't find a clear normative reference for it. From a C++ user perspective the example above makes sense, however from a quality of implementation (QoI) perspective it may not: it would require two-pass template argument deduction where first a parameter pack is unambigously deduced whereafter the result is inserted into the function parameter list to know other template arguments that could be deduced from the modified (by the first thesis) argument list. This is not how template argument deduction works anywehere else, possibly solely due to QoI.Versed
Do you think f(Pack<int, int>{}, Pack<int>{}, 5, 5, 5); should also compile? It doesn't.Netta
@n.1.8e9-where's-my-sharem. My take is that compilers seem to deduce some parameter pack which appear (somewhere) in a non-deduced context as being empty, even if they can be deduced from elsewhere; possible due to the "the type of that pack is never deduced" of [temp.deduct.call]/1. However, curiously they do accept the same situation when there's only a single parameter pack (DEMO).Versed
V
2

std::type_identity<TT>... is mentioned in the comments as an way to remove the third function parameter (which is a pack) from argument deduction, but this has no effect as a function parameter pack that does not occur at the end of a parameter list is in a non-deduced context anyway; as per [temp.deduct.type]/5.7:

/5 The non-deduced contexts are:

  • [...]
  • /5.7 A function parameter pack that does not occur at the end of the parameter-declaration-list.

Then why can't the parameter pack be unambiguously deduced from the first function parameter (Pack<TT...>)? This would arguably make sense for C++ developers, however it may end up as a quality of implementation issue for implementors, as it would start mixing template argument deduction with overload resolution. It would require two-pass template argument deduction where first a parameter pack is unambigously deduced whereafter from say one function parameter, after which the result is used to modify the same function's parameter list (to expand an otherwise non-deducible parameter pack) and thereafter return to template argument deduction for the modified function template. This is not how template argument deduction works anywhere else, possibly solely due to quality of implementation.

[temp.deduct.call]/1 does mention that such a pack is never deduced (emphasis mine):

[...] When a function parameter pack appears in a non-deduced context ([temp.deduct.type]), the type of that pack is never deduced.

It could be argued whether or not this intentionally rejects packs that could be deduced from anywhere else. If so, the OP's program is indeed ill-formed, however the error messages of the compilers is not very helpful in diagnosing this as the root cause (if it it). It seems as if compilers may rely on [temp.deduct.call]/1 to actually deduce non-deducible template parameter packs to empty packs.


We may finally note that [temp.deduct.call]/1 supports the use of explicit template arguments, as this is not deduction:

#include <type_traits>

template<typename... T> struct Pack { };

// Note the swap of template argument positions
// to allow explicitly providing template arguments.
template<typename T, typename... TT>
void f(Pack<TT...>, Pack<T>, std::type_identity_t<TT>..., std::type_identity_t<T>) {}

int main() {
    f<int, int, int>(Pack<int, int>{}, Pack<int>{}, 1, 2, 3);
}
Versed answered 13/6, 2022 at 12:52 Comment(4)
"Here, TT appears in a non-deduced context" No, it doesn't, it's in the end of the parameter list.Netta
@n.1.8e9-where's-my-sharem. It is non-deduced due to another clause of [temp.deduct.type]/5.1: note the std::type_identity_t.Versed
Yes you are right, TT appears in a non-deduced context (and also in a deduced context), but TT is not a function parameter pack. Aside: this part of the standard is such a fine mess, I have no idea what's going on in there. (As if other parts are much better).Netta
@n.1.8e9-where's-my-sharem. I removed that example as I'm also not sure whether it falls under this discussion. Its second function parameter was an unnamed function parameter pack of type std::type_identity_t<TT>..., where the template parameter TT is in a non-deduced context for each expanded function parameter, but I agree, I don't know if that falls under the function parameter pack itself being in a non-deduced context. Confusion.Versed

© 2022 - 2024 — McMap. All rights reserved.