Search code examples
c++20c++-conceptsexplicit-instantiationtemplate-instantiationimplicit-instantiation

Does C++20 support declaring an explicit function template instantiation using constraints after the template has been referenced in another template?


Context

In C++, we seem to be able to declare/define explicit function template instantiations after the function template has been referenced in another template function, if the other template function has not been instantiated for the specialized type,

template<typename T> consteval int x() { return 0; }
template<typename T> consteval int y() { return x<T>(); }

struct TypeA {};
template<> consteval int x<TypeA>() { return 1; }

static_assert(y<TypeA>() == 1); // PASS

In the above example, my reasoning to why the static_assert succeeds is,

  1. x<TypeA>'s point of instantiation is at y<TypeA>'s instantiation

n4868 [temp.point]/1 For a function template specialization ..., if the specialization is implicitly instantiated because it is referenced from within another template specialization and the context from which it is referenced depends on a template parameter, the point of instantiation of the specialization is the point of instantiation of the enclosing specialization.

  1. x<TypeA>'s instantiation context contains y<TypeA>'s instantiation context

n4868 [module.context]/3 During the implicit instantiation of a template whose point of instantiation is specified as that of an enclosing specialization ([temp.point]), the instantiation context is the union of the instantiation context of the enclosing specialization ...

  1. static_assert evaluates the enclosed constant-expression

n4868 [temp.inst]/8 The existence of a definition of a variable or function is considered to affect the semantics of the program if the variable or function is needed for constant evaluation by an expression ([expr.const]), even if constant evaluation of the expression is not required or if constant expression evaluation does not use the definition.

  1. static_assert's evaluation triggers y<TypeA>'s instantiation

n4868 [temp.inst]/5 Unless a function template specialization is a declared specialization, the function template specialization is implicitly instantiated when the specialization is referenced in a context that requires a function definition to exist or if the existence of the definition affects the semantics of the program.

Problem

C++20 introduced template constraints [temp.constr.constr] with the requires keyword to specify constraints on templates. The above explicit specialization pattern no longer seems to work when we declare/define our specialization using constraints after g,

#include <concepts>

template<typename T> consteval int f() { return 0; }
template<typename T> consteval int g() { return f<T>(); }

struct TypeB {};
template<typename T> requires std::same_as<T, TypeB> consteval int f() { return 1; }

static_assert(g<TypeB>() == 1); // FAIL

From the standard's perspective, is this pattern (in both above samples) ever supported? Using the above reasoning it appears to me that we should have supported both, while only the first sample compiles on GCC/MSVC/LLVM.

Any help/direction would be greatly appreciated!

Additional Context

The snippet involving constraints do not compile on x86-64 GCC(13.2), MSVC(v19.37) and LLVM(17.0.1) with C++20 enabled.

AST

Looking a bit closer at the AST produced by LLVM,

|-FunctionTemplateDecl <line:3:1, col:52> col:36 f
| |-TemplateTypeParmDecl <col:10, col:19> col:19 typename depth 0 index 0 T
| |-FunctionDecl <col:22, col:52> col:36 consteval f 'int ()' implicit-inline
| | `-CompoundStmt <col:40, col:52>
| |   `-ReturnStmt <col:42, col:49>
| |     `-IntegerLiteral <col:49> 'int' 0
| `-FunctionDecl <col:22, col:52> col:36 used consteval f 'int ()' implicit-inline
|   |-TemplateArgument type 'TypeB'
|   | `-RecordType 'TypeB'
|   |   `-CXXRecord 'TypeB'
|   `-CompoundStmt <col:40, col:52>
|     `-ReturnStmt <col:42, col:49>
|       `-IntegerLiteral <col:49> 'int' 0
...
|-FunctionTemplateDecl <line:7:1, col:84> col:68 f
| |-TemplateTypeParmDecl <col:10, col:19> col:19 referenced typename depth 0 index 0 T
| |-ConceptSpecializationExpr <col:31, col:52> 'bool' Concept 0xc3c4268 'same_as'
...
| | |-TemplateArgument <line:7:44> type 'T'
| | | `-TemplateTypeParmType 'T' dependent depth 0 index 0
| | |   `-TemplateTypeParm 'T'
| | `-TemplateArgument <col:47> type 'TypeB':'TypeB'
| |   `-ElaboratedType 'TypeB' sugar
| |     `-RecordType 'TypeB'
| |       `-CXXRecord 'TypeB'
| `-FunctionDecl <col:54, col:84> col:68 consteval f 'int ()' implicit-inline
|   `-CompoundStmt <col:72, col:84>
|     `-ReturnStmt <col:74, col:81>
|       `-IntegerLiteral <col:81> 'int' 1

it appears we implicitly instantiated f<TypeB> first using the generic f<T>, we then instantiated it again using f<T> requires std::same_as<T, TypeB>
=> breaks one-definition-rule, undefined behavior

Forward declare for the specialization with constraints

If we forward declare the function template specialization with constraints, compilers no longer generate the implicit instantiation of f<TypeB> -> 0.

#include <concepts>

template<typename T> consteval int f() { return 0; }

struct TypeB {};
template<typename T> requires std::same_as<T, TypeB> consteval int f();

template<typename T> consteval int g() { return f<T>(); }

template<typename T> requires std::same_as<T, TypeB> consteval int f() { return 1; }

static_assert(g<TypeB>() == 1); // PASS

It seems to indicate the instantiation point of g<TypeB> is at g's definition time, i.e., g<TypeB> is instantiated (and uses the name lookup & declarations available) at template<typename T> consteval int g() {...} instead of at the static_assert.
However, this behavior is different from the first snippet where we directly specialize template<> consteval int x<TypeA>() { return 1; } instead of using template constraints. Is such difference is intended by the standard?


Solution

  • You are confusing several template-related terms.

    Nowhere in your shown code is there any explicit instantiation. An explicit instantiation would start with template, not followed by <.

    Neither

    template<typename T> consteval int f();
    

    nor

    template<typename T> requires std::same_as<T, TypeB> consteval int f();
    

    are explicit specializations either. Both are primary declarations of function templates. The declarations are not equivalent because they do not have equivalent requires clauses and therefore declare two completely separate function templates that overload one another.

    template<> consteval int f<TypeA>() { return 1; }
    

    is an explicit specialization for the first template, because it starts with template<> (nothing between < and >!) and because template argument deduction of the type int() against the type of the previously-declared f function template is possible and satisfies its non-existent constraints, given that the only template parameter is already explicitly specified.

    template<typename T> requires std::same_as<T, TypeB> consteval int f() { return 1; }
    

    is not an explicit specialization at all, since it doesn't start with template<>. It is a declaration of a new, distinct function template overload, as mentioned above.

    A requires clause on an explicit specialization declaration isn't syntactically permitted in the first place. Instead explicit specializations are matched to previously-declared function templates via template argument deduction (including constraint satisfaction) of their function types and partial ordering of function templates (including partial ordering on constraints).

    In particular, if you had declared both of these function templates before the explicit specialization, then the explicit specialization would be for the constrained function template, not the unconstrained one as in your first example.


    In your first example you are calling the unconstrained x (f) function template, which is declared before y (g) and it uses the explicit specialization of that function template. That the explicit specialization is declared later is permitted as long as it is declared before the point of instantiation of the call.

    In your example under "Problem" f<TypeB>() is calling the first template, rather than the second one, because lookup of a dependent name from a template context is generally done from the point of definition. Only for ADL is lookup done from the point of instantiation. The lookup will therefore find only the unconstrained f function template, not the constrained one which is declared later and has no relation to the unconstrained one.

    In your example at the end you are calling the constrained function template, since you now declared it before the use in g. You could remove the unconstrained declarations of f. This function template is unused.

    In none of your examples is the constrained f function template explicitly specialized.

    In neither case does this really have anything to do with points of instantiation.