In C++ why can't I take the address of a function that also has a templated version?

351 Views Asked by At

Here is a piece of code that tries to provide a pointer to an overloaded function to another function that can accept a value of any type:

template <typename T>
void AcceptAnything(T&&);

void Foo();

template <typename T>
void Foo();

void Bar() {
  AcceptAnything(&Foo);
}

This fails to compile, giving the following error under clang:

<source>:10:3: error: no matching function for call to 'AcceptAnything'
   10 |   AcceptAnything(&Foo);
      |   ^~~~~~~~~~~~~~
<source>:2:6: note: candidate template ignored: couldn't infer template argument 'T'
    2 | void AcceptAnything(T&&);
      |      ^

I understand why you can't do this for an overloaded function in general. But in this particular case I'm surprised that &Foo doesn't have type void(*)(), given that no template arguments are provided. My mental model is:

  • The only function named Foo here is the untemplated one, with the other one being a family of functions named Foo<T> for any type T.

  • The expression &Foo refers to the address of a particular function named Foo.

  • There is no such thing as the address of a family of functions.

So I would expect that &Foo unambiguously means "a pointer to the untemplated function Foo". But both gcc and clang disagree with me, so my mental model must be wrong.

Why is &Foo ambiguous in this case, and which part of the standard covers it?

3

There are 3 best solutions below

5
user12002570 On

This is CWG 2873 and current wording seems to make the program well-formed even though all compilers reject the code.

The non-template function is the only viable option here since template argument deduction fails here for function template Foo and the phrase "if any" in over.over#3 seems to mean that template are ignored if deduction fails. So there is no specialization that can be added to the set and the set contains only one candidate which is the non-template function.


Now as there is no target from the function template Foo, the template argument deduction for Foo fails so no specialization will be added to set and as per over.over#3 the template will be ignored:

The specialization, if any, generated by template argument deduction ([temp.over], [temp.deduct.funcaddr], [temp.arg.explicit]) for each function template named is added to the set of selected functions considered.

Note the emphasis on "if any" in the above quoted reference.

Since template argument deduction fails here, there is no specialization that is added to the set. And the only candidate in the set is the non-template function which will be used.

8
wearetherobots On

The expression AcceptAnything(&Foo); attempts to do two things:

  1. Deduce the parameter type for AcceptAnything function template,
  2. Determine the address of the overloaded set Foo, thats is what &Foo refers to.

The reason why &Foo is not resolved to the address of the function Foo is because the target (the parameter of the function AcceptAnaything()) is not determined explicitly.

Because AcceptAnything() is a template function, the call requires determining which template instantiation it refers to. The compiler must deduce the template parameter and it uses the value passed to the function parameter to do this. However, the standard specifies that when this happens with a value whose type is the address of the function, and the potential candidates (a.k.a. overload set) contain function template, then the deduction fails. As the deduction fails, the parameter type cannot be used to constrain the resolution of what &Foo refers to.

It is circular... but that's my understanding. More details and relevant extracts from the standard are below.


Note that gcc gives a more explicit error:

error: no matching function for call to 'AcceptAnything(<unresolved overloaded function type>)'

The issue is similar with a statement as auto f = &Foo, which cannot be resolved either, although the error message is less explicit in that case.

To give a different perspective on the issue, note that the following code compiles, because it makes (1) explicit, and therefore allows the compiler to resolve the overload set in (2):

AcceptAnything<void()>(&Foo);

The reaons why this works is similar to why the resolution can happen in the following statement: void (*f)() = &Foo;.

See repro: godbolt


relevant extracts from standard

If (2) is done independently of (1), we would have a target type that would resolve the address of the overload set, and discard the function template named Foo. See over.over:

An id-expression whose terminal name refers to an overload set S and that appears without arguments is resolved to a function, a pointer to function, or a pointer to member function for a specific function that is chosen from a set of functions selected from S determined based on the target type required in the context (if any), as described below. The target can be

  • (1.1) an object or reference being initialized ([dcl.init], [dcl.init.ref], [dcl.init.list]),
  • (1.2) the left side of an assignment,
  • (1.3) a parameter of a function ([expr.call]),
  • (1.4) a parameter of a user-defined operator,
  • (1.5) the return value of a function, operator function, or conversion ([stmt.return]),
  • (1.6) an explicit type conversion ([expr.type.conv], [expr.static.cast], [expr.cast]), or
  • (1.7) a non-type template-parameter ([temp.arg.nontype]).

But in this situation, the type of the target (the parameter of the function) needs to be determined. Also note temp.deduct.funcaddr:

Template arguments can be deduced from the type specified when taking the address of an overload set. If there is a target, the function template's function type and the target type are used as the types of P and A, and the deduction is done as described in [temp.deduct.type]. Otherwise, deduction is performed with empty sets of types P and A.

In this case, the template type deduction fails. See temp.deduct.type:

In most cases, the types, templates, and non-type values that are used to compose P participate in template argument deduction. That is, they may be used to determine the value of a template argument, and template argument deduction fails if the value so determined is not consistent with the values determined elsewhere. In certain contexts, however, the value does not participate in type deduction, but instead uses the values of template arguments that were either deduced elsewhere or explicitly specified. If a template parameter is used only in non-deduced contexts and is not explicitly specified, template argument deduction fails.

and

The non-deduced contexts are:

[...]

  • (5.6) A function parameter for which the associated argument is an overload set ([over.over]), and one or more of the following apply:
    • (5.6.1) more than one function matches the function parameter type (resulting in an ambiguous deduction), or
    • (5.6.2) no function matches the function parameter type, or
    • (5.6.3) the overload set supplied as an argument contains one or more function templates.

[...]

0
Enlico On

Not language lawyer here, but given the code snippet in the OP, the function template Foo doesn't undergo template argument deduction, so I think the answer is more straightforwardly contained in over.over#3:

The specialization, if any, generated by template argument deduction ([temp.over], [temp.deduct.funcaddr], [temp.arg.explicit]) for each function template named is added to the set of selected functions considered.

Because no specialization of the tempalted Foo has ever been produced, the set of selected functions considered truly only contains the non-templated Foo.