Overload resolution and partial template ordering

931 views Asked by At

Consider this simple pair of function templates.

template <typename T>
void foo(T& ) { std::cout << __PRETTY_FUNCTION__ << '\n'; }

template <typename C>
void foo(const C& ) { std::cout << __PRETTY_FUNCTION__ << '\n'; }

If we call foo with a non-const argument:

int i = 4;
foo(i);

The T& overload is preferred based on [over.ics.rank]/3.2.6, since the deduced reference int& is less cv-qualified than the deduced reference const int&.

However, if we call foo with a const argument:

const int ci = 42;
foo(ci);

The const C& overload is preferred because it is "more specialized" based on [over.match.best]/1.7. But what are the rules to determine this? My understanding was that you synthesize a type for C (call it, M) and try to perform deduction on foo(M) - but that would succeed (with T == M). It's only an rvalue that would cause that deduction to fail - but how does the compiler know that it has to choose an rvalue in the synthesis step?

1

There are 1 answers

0
Columbo On BEST ANSWER

Disclaimer: The types we consider are always types of parameters. The types/value categories/etc. of the actual arguments passed are solely considered in overload resolution, never in partial ordering.

Partial ordering considers both overloads in two "turns", in which one template is always the parameter template, and the other template is the argument template. [temp.deduct.partial]/2:

For each of the templates involved there is the original function type and the transformed function type. [..] The deduction process uses the transformed type as the argument template and the original type of the other template as the parameter template. This process is done twice for each type involved in the partial ordering comparison: once using the transformed template-1 as the argument template and template-2 as the parameter template and again using the transformed template-2 as the argument template and template-1 as the parameter template.

You should be familiar with the way transformed "templates" are generated. This is specified in §14.5.6.2/3.

To produce the transformed template, for each type, non-type, or template template parameter (including template parameter packs (14.5.3) thereof) synthesize a unique type, value, or class template respectively and substitute it for each occurrence of that parameter in the function type of the template.

So our (transformed) argument templates are

void foo( Unique1& );

void foo( Unique2 const& );

[temp.deduct.partial]/3 & /4:

The types used to determine the ordering depend on the context in which the partial ordering is done:

  • In the context of a function call, the types used are those function parameter types for which the function call has arguments. [..]

Each type nominated above from the parameter template and the corresponding type from the argument template are used as the types of P and A.

Thus we have two turns, and in both we have a type P and a type A:

Turn 1:
    P1:   T const&
    A1:   Unique1&

Turn 2:
    P2:   T&
    A2:   Unique2 const&

But before the fun begins, some transformations are performed on these types as well - I abbreviated [temp.deduct.partial]/5 & /7:

  • If P or A are references, then they're replaced by the type they refer to.
  • Any top-level cv-qualifiers are removed.

Note that the removed cv-qualifiers are "remembered" for later. [temp.deduct.partial]/6:

If both P and A were reference types (before being replaced with the type referred to above), determine which of the two types (if any) is more cv-qualified than the other; otherwise the types are considered to be equally cv-qualified for partial ordering purposes. The result of this determination will be used below.

Thus we're left with

Turn 1:
    P1:   T
    A1:   Unique1

Turn 2:
    P2:   T
    A2:   Unique2

Now we perform deduction - which clearly succeeds in both turns by setting T=Unique[1/2]. From [temp.deduct.partial]/8:

If deduction succeeds for a given type, the type from the argument template is considered to be at least as specialized as the type from the parameter template.

That gives us both that Unique1& is at least as specialized as T const&, and that Unique2 const& is at least as specialized as T&.


However, this is where [temp.deduct.partial]/(9.2) steps in:

If, for a given type, deduction succeeds in both directions (i.e., the types are identical after the transformations above) and both P and A were reference types (before being replaced with the type referred to above):

  • [..]; otherwise,

  • if the type from the argument template is more cv-qualified than the type from the parameter template (as described above), the parameter type is not considered to be at least as specialized as the argument type.

The remembered cv-qualifiers come into play. A2 is "more cv-qualified (as described above)" than P2, hence P2 is not considered to be at least as specialized as A2.

Finally, [temp.deduct.partial]/10:

Function template F is at least as specialized as function template G if, for each pair of types used to determine the ordering, the type from F is at least as specialized as the type from G.
F is more specialized than G if F is at least as specialized as G and G is not at least as specialized as F.

implies that since the type T& is not at least as specialized as Unique2 const& and we already established that T const& is at least as specialized as Unique1&, the T const&-overload is more specialized than the T&-overload.


The aforementioned rule in paragraph 9 is currently subject of CWG #2088 created four months ago by R. Smith:

The late tiebreakers for lvalue-vs-rvalue references and cv-qualification in 14.8.2.4 [temp.deduct.partial] paragraph 9 are applied

If, for a given type, deduction succeeds in both directions (i.e., the types are identical after the transformations above) and both P and A were reference types (before being replaced with the type referred to above):

However, this is based on a false assumption. [..] We need to decide whether the rule is “deduction succeeds in both directions” or “the types are identical.” The latter seems more reasonable.

This will not alter the result established though, since the types we got are indeed identical.