6

Note: this question is about explicit instantiation, not explicit specialization.

Please take a look at the following example:

template <bool A, typename X>
void f (X &x) {} // 1
template <bool A>
void f (int &x) {} // 2
template void f<true> (int &x); // 3

Let's say that my initial goal was to explicitly instantiate only second function template for A = true so I write line // 3. However intuitively first definition also could be explicitly instantiated with line // 3 which is a bit problematic because I can't actually escape it with current syntax since bool A could not be deduced in my case. Theoretically I would not even mind if both function templates end up being explicitly instantiated but the most interesting part is the actual compilation results.

(In all the cases where compilation is successful, only second function template gets instantiated.)

  1. Original case. Compiles with msvc and clang. Fails to compile with gcc with:

    error: ambiguous template specialization 'f' for 'void f(int&)'

  2. Replacing bool A with bool A = true in first function template makes gcc compile it.

  3. Replacing X &x with X &&x (forwarding reference) makes clang fail to compile it with:

    error: partial ordering for explicit instantiation of 'f' is ambiguous

Here's the demo for the most drastic cases.

(The latest versions for the compilers available on godbolt were used)

So my question is - does explicit instantiation behavior for such cases is really so weakly specified making it easy to walk into this kind of minefield or maybe msvc is the most standard conforming? Personally I don't feel like my initial goal was that otherworldly even if a bit conflicting with the current syntax.

L. F.
  • 19,445
  • 8
  • 48
  • 82
Predelnik
  • 5,066
  • 2
  • 24
  • 36
  • 1
    @Predelnik I picked the wrong tag, NathanOliver didn't. I am sorry. – L. F. Jul 25 '19 at 13:44
  • "*intuitively first definition also could be explicitly instantiated with line*" Wouldn't that be a *partial* specialization, rather than a full specialization of the first template, since you don't explicitly specify all of the template parameters? And aren't partial specialization of functions not allowed? – Nicol Bolas Jul 25 '19 at 13:57
  • 1
    @NicolBolas it could deduce the argument type and once again it's not specialization but instantiation. – Predelnik Jul 25 '19 at 13:59
  • The function can be called without ambiguity on all three compilers: https://gcc.godbolt.org/z/5O-3Jx – L. F. Jul 25 '19 at 14:14
  • @L.F. It can be obviously, but in real life scenario where it was encountered explicit instantiation was happening in different translation unit of course. This example is simplified and mostly about what I should except from the language, usefullness of the construct itself is secondary. – Predelnik Jul 25 '19 at 14:26
  • This is a GCC bug: https://gcc.gnu.org/bugzilla/show_bug.cgi?id=39270 – Fedor Jan 23 '22 at 20:07

1 Answers1

3

Basically, an explicit instantiation directive is interpreted according to the following 3 step procedure:

  1. Perform name lookup on the name of the explicit instantiation to determine a list of "candidate" templates.
  2. Perform template argument deduction to determine whether any of those templates are "viable".
  3. If there are multiple "viable" templates (this can only occur in the case of a function template, since other kinds of templates cannot be overloaded), but one of them is more specialized than all the other ones, the explicit instantiation directive explicitly instantiates that template. If not, then the program is ill-formed.

I borrowed the terms "candidate" and "viable" from overload resolution terminology. They are in scare quotes to remind you that I am using them in a way in which the standard doesn't.

We'll now go over the language wording for each of these.

Step 1 is not that relevant to your question, since obviously the f in // 3 has // 1 and // 2 as candidates. But there are explicit rules about this in the C++23 draft (I don't think they're in C++17 or C++20), [dcl.meaning.general]/3:

Otherwise:

  • If the id-expression in the declarator-id of the declarator is a qualified-id Q, let S be its lookup context (6.5.5); the declaration shall inhabit a namespace scope.
  • Otherwise, let S be the entity associated with the scope inhabited by the declarator.
  • If the declarator declares an explicit instantiation or a partial or explicit specialization, the declarator does not bind a name. If it declares a class member, the terminal name of the declarator-id is not looked up; otherwise, only those lookup results that are nominable in S are considered when identifying any function template specialization being declared (13.10.3.7).

[...]

In your case, the declarator-id in // 3 is unqualified, so S is the namespace that these declarations inhabit (e.g., if your code represents a complete translation unit, then S is the global namespace) and the "candidates" are the results of unqualified name lookup for f that are nominable in S. According to [basic.scope.scope]/6, only those declarations that have a target scope of S or a namespace that belongs to the inline namespace set of S are nominable [1]. Since // 1 and // 2 have a target scope of S, they are candidates.

Steps 2 and 3 are described by C++17 [temp.deduct.decl] (I refer to C++17 because that was the current standard at the time this question was posted):

In a declaration whose declarator-id refers to a specialization of a function template, template argument deduction is performed to identify the specialization to which the declaration refers. Specifically, this is done for explicit instantiations (17.7.2), explicit specializations (17.7.3), and certain friend declarations (17.5.4). This is also done to determine whether a deallocation function template specialization matches a placement operator new (6.7.4.2, 8.3.4). In all these cases, P is the type of the function template being considered as a potential match and A is either the function type from the declaration or the type of the deallocation function that would match the placement operator new as described in 8.3.4. The deduction is done as described in 17.8.2.5.

If, for the set of function templates so considered, there is either no match or more than one match after partial ordering has been considered (17.5.6.2), deduction fails and, in the declaration cases, the program is ill-formed.

I'll skip over the details of the type deduction since it's obvious that it succeeds with both // 1 and // 2, where bool A is obtained from the explicitly specified template argument true and in the case of // 1, X is deduced as int.

So finally we have step 3: partial ordering, as described in 17.5.6.2 ([temp.func.order]). I quote the relevant passages:

Partial ordering selects which of two function templates is more specialized than the other by transforming each template in turn (see next paragraph) and performing template argument deduction using the function type. The deduction process determines whether one of the templates is more specialized than the other. If so, the more specialized template is the one chosen by the partial ordering process.

To produce the transformed template, for each type, non-type, or template template parameter (including template parameter packs (17.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. [...]

Using the transformed function template’s function type, perform type deduction against the other template as described in 17.8.2.4.

17.8.2.4 [temp.deduct.partial] explains how the result of the deduction is used to determine which template is more specialized (p8):

[...] 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.

p12 contains the following important proviso:

In most cases, deduction fails if not all template parameters have values, but for partial ordering purposes a template parameter may remain without a value provided it is not used in the types being used for partial ordering. [Note: A template parameter used in a non-deduced context is considered used. — end note]
[Example:

template <class T> T f(int);           // 1
template <class T, class U> T f(U);    // 2
void g() {
  f<int>(1);                           // calls #1
}

end example]

In the example given in the standard, we are in the context of a function call, so the types used for the deduction are the function parameter types for which arguments were provided (p3.1). Since T (in both templates) is only used in the return type T, it is not part of any of the types used for deduction. That means that the deduction process succeeds when deducing // 2 from // 1 (because U can be deduced as int) and doesn't succeed in the other direction. That implies that // 1 is more specialized.

In the OP's code, the types of the functions themselves are used because this is not a function call context (p3.1). A cannot be deduced in either direction, but it also doesn't participate in the function types, so the fact that it can't be deduced is ignored. Clearly, X can be deduced in // 1 from the type of // 2. This tells us that // 2 is at least as specialized as // 1. If we do it the other way around, the unique type synthesized for X will not match the int in // 2. So // 1 is not at least as specialized as // 2.

Clang and MSVC are right. // 2, being the unambiguously more specialized template, is explicitly instantiated. GCC is wrong. Fedor provided (in the comments) a link to the GCC bug report. Comment 3 is very similar to your example.

[1] The nominability requirement stated here—both for qualified and unqualified declarator-ids—basically just says that you have to state the exact scope of the entity you're instantiating or specializing. If you use an unqualified name f, then it won't explicitly instantiate any template named f from an enclosing namespace. If you use a qualified name such as N::f, it won't explicitly instantiate an f from an enclosing namespace of N. This is how compilers have worked since C++98 but I don't think the rules were ever explicitly written down before C++23.

Brian Bi
  • 111,498
  • 10
  • 176
  • 312