Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Template argument deduction for variadic function pointer parameter - handling of ambiguous cases

Consider the following code:

#include <iostream>

void f(int) { }

void f(int, short) { }

template<typename... Ts> void g(void (*)(Ts...))
{
   std::cout << sizeof...(Ts) << '\n';
}

template<typename T, typename... Ts> void h(void (*)(T, Ts...))
{
   std::cout << sizeof...(Ts) << '\n';
}

int main()
{
   g(f);         // #1
   g<int>(f);    // #2
   h(f);         // #3
   h<int>(f);    // #4
}

The intent is to try each of the lines in the body of main() separately. My expectations were that all four calls were ambiguous and would result in compiler errors.

I tested the code on:

  • Clang 3.6.0 and GCC 4.9.2, both using -Wall -Wextra -pedantic -std=c++14 (-std=c++1y for GCC) - same behaviour in all these cases, except for minor differences in the wording of error messages;
  • Visual C++ 2013 Update 4 and Visual C++ 2015 CTP6 - again, same behaviour, so I'll call them "MSVC" going forward.

Clang and GCC:

  • #1: Compiler error, with a confusing message, basically no overload of 'f' matching 'void (*)()'. What? Where did the no-param declaration come from?
  • #3: Compiler error, with another confusing message: couldn't infer template argument 'T'. Of all the things that could fail there, deducing the argument for T would be the last one I would expect...
  • #2 and #4: Compiles with no errors and no warnings, and chooses the first overload.

For all four cases, if we eliminate one of the overloads (any one), the code compiles fine and chooses the remaining function. This looks like an inconsistency in Clang and GCC: after all, if deduction succeeds for both overloads separately, how can one be chosen over the other in cases #2 and #4? Aren't they both perfect matches?

Now, MSVC:

  • #1, #3 and #4: Compiler error, with a nice message: cannot deduce template argument as function argument is ambiguous. Now that's what I'm talking about! But, wait...

  • #2: Compiles with no errors and no warnings, and chooses the first overload. Trying the two overloads separately, only the first one matches. The second one generates an error: cannot convert argument 1 from 'void (*)(int,short)' to 'void (*)(int)'. Not so good anymore.

To clarify what I'm looking for with case #2, this is what the standard (N4296, first draft after C++14 final) says in [14.8.1p9]:

Template argument deduction can extend the sequence of template arguments corresponding to a template parameter pack, even when the sequence contains explicitly specified template arguments.

Looks like this part doesn't quite work in MSVC, making it choose the first overload for #2.

So far, it looks like MSVC, while not quite right, is at least relatively consistent. What's going on with Clang and GCC? What's the correct behaviour according to the standard for each case?

like image 841
bogdan Avatar asked Apr 02 '15 16:04

bogdan


People also ask

What is template argument deduction?

Template argument deduction is used in declarations of functions, when deducing the meaning of the auto specifier in the function's return type, from the return statement.

What is Variadic template in C++?

Variadic templates are class or function templates, that can take any variable(zero or more) number of arguments. In C++, templates can have a fixed number of parameters only that have to be specified at the time of declaration.

What is the use of Variadic templates?

With the variadic templates feature, you can define class or function templates that have any number (including zero) of parameters. To achieve this goal, this feature introduces a kind of parameter called parameter pack to represent a list of zero or more parameters for templates.


1 Answers

As far as I can tell, Clang and GCC are right in all four cases according to the standard, even though their behaviour may seem counter-intuitive, especially in cases #2 and #4.

There are two main steps in the analysis of the function calls in the code sample. The first one is template argument deduction and substitution. When that completes, it yields a declaration of a specialization (of either g or h) where all template parameters have been replaced with actual types.

Then, the second step attempts to match f's overloads against the actual pointer-to-function parameter that was constructed in the previous step. The best match is chosen according to the rules in [13.4] - Address of overloaded function; in our cases this is pretty simple, as there are no templates among the overloads, so we have either one perfect match or none at all.

The key point to understanding what happens here is that an ambiguity in the first step doesn't necessarily mean that the whole process fails.

The quotes below are from N4296 but the content hasn't changed since C++11.

[14.8.2.1p6] describes the process of template argument deduction when a function parameter is a pointer to function (emphasis mine):

When P is a function type, pointer to function type, or pointer to member function type:
— If the argument is an overload set containing one or more function templates, the parameter is treated as a non-deduced context.
— If the argument is an overload set (not containing function templates), trial argument deduction is attempted using each of the members of the set. If deduction succeeds for only one of the overload set members, that member is used as the argument value for the deduction. If deduction succeeds for more than one member of the overload set the parameter is treated as a non-deduced context.

For completeness, [14.8.2.5p5] clarifies that the same rule applies even when there's no match:

The non-deduced contexts are: [...]
— A function parameter for which argument deduction cannot be done because the associated function argument is a function, or a set of overloaded functions (13.4), and one or more of the following apply:
— more than one function matches the function parameter type (resulting in an ambiguous deduction), or
— no function matches the function parameter type, or
— the set of functions supplied as an argument contains one or more function templates.

So, no hard errors because of ambiguity in these cases. Instead, all template parameters are in non-deduced contexts in all our cases. This combines with [14.8.1p3]:

[...] A trailing template parameter pack (14.5.3) not otherwise deduced will be deduced to an empty sequence of template arguments. [...]

While the use of the word "deduced" is confusing here, I take this to mean that a template parameter pack is set to the empty sequence if no elements can be deduced for it from any source and there are no template arguments explicitly specified for it.

Now, the error messages from Clang and GCC start to make sense (an error message that only makes sense after you understand why the error occurs is not exactly the definition of a helpful error message, but I guess it's better than nothing):

  • #1: Since Ts is the empty sequence, the parameter of g's specialization is indeed void (*)() in this case. The compiler then tries to match one of the overloads to the destination type and fails.
  • #3: T only appears in a non-deduced context and is not explicitly specified (and it's not a parameter pack, so it cannot be "empty"), so a specialization declaration cannot be constructed for h, hence the message.

For the cases that do compile:

  • #2: Ts cannot be deduced, but one template parameter is explicitly specified for it, so Ts is int, making g's specialization's parameter void (*)(int). The overloads are then matched against this destination type, and the first one is chosen.
  • #4: T is explicitly specified as int and Ts is the empty sequence, so h's specialization's parameter is void (*)(int), the same as above.

When we eliminate one of the overloads, we eliminate the ambiguity during template argument deduction, so the template parameters are no longer in non-deduced contexts, allowing them to be deduced according to the remaining overload.

A quick verification is that adding a third overload

void f() { }

allows case #1 to compile, which is consistent with all of the above.

I suppose things were specified this way to allow template arguments involved in pointer-to-function parameters to be obtained from other sources, like other function arguments or explicitly-specified template arguments, even when template argument deduction can't be done based on the pointer-to-function parameter itself. This allows a function template specialization declaration to be constructed in more cases. Since the overloads are then matched against the parameter of the synthesized specialization, this means we have a way to select an overload even if template argument deduction is ambiguous. Quite useful if this is what you're after, terribly confusing in some other cases - nothing unusual, really.

The funny thing is that MSVC's error message, while apparently nice and helpful, is actually misleading for #1, somewhat but not quite helpful for #3, and incorrect for #4. Also, its behaviour for #2 is a side effect of a separate problem in its implementation, as explained in the question; if it weren't for that, it would probably issue the same incorrect error message for #2 as well.

This is not to say that I like Clang's and GCC's error messages for #1 and #3; I think they should at least include a note about the non-deduced context and the reason it occurs.

like image 200
bogdan Avatar answered Oct 18 '22 05:10

bogdan