Point of instantiation of constexpr function template

With the increased use of constexpr across the standard library in recent C++ standards, I occasionally run into code that starts to fail when building with Clang (esp. when building with -std=c++20 or -std=c++2b against recent trunk libc++ or libstdc++) but not with GCC.

I think a faithful stripped-down reproducer is a test.cc

struct S;
template<typename T> constexpr void f1() { T::f(); }
void f2() { f1<S>(); }
struct S { static void f(); };

for which

$ g++ -c test.cc

succeeds while

$ clang++ -c test.cc
test.cc:2:44: error: incomplete type 'S' named in nested name specifier
template<typename T> constexpr void f1() { T::f(); }
                                           ^~~
test.cc:3:13: note: in instantiation of function template specialization 'f1<S>' requested here
void f2() { f1<S>(); }
            ^
test.cc:1:8: note: forward declaration of 'S'
struct S;
       ^
1 error generated.

fails.

The reasoning given in the comments in Sema::MarkFunctionReferenced (clang/lib/Sema/SemaExpr.cpp) looks compelling to me, making it look like Clang would be correct in failing and GCC would be wrong in succeeding? But I’m not actually sure I understand things right…

Also, if the constexpr is removed, both Clang and GCC succeed (i.e., they apparently only implicitly instantiate f1<S> at the end of the translation unit, after the definition of S). Is the translation unit indeed well-formed then, or is this a case of “ill-formed, no diagnostic required” where both Clang and GCC happen to pick a point of instantiation that comes after S has been defined, not before?

Hi! Calls to constexpr functions are indeed make them instantiating immediately. Here is the condition in the code:
https://github.com/llvm/llvm-project/blob/87abc5013ac05a8b7ef86ee98ffcabdd8908802c/clang/lib/Sema/SemaExpr.cpp#L18103-L18107
I guees this is done in purpose of code simplicity.

I found a case where GCC also fails in the same way as Clang: when we declare constexpr variable - Compiler Explorer

Thanks for that pointer!

But that

struct S;
template<typename T> constexpr int f1() { return sizeof(T); }
constexpr int size = f1<S>();
struct S { static void f(); };

is a rather different scenario, where the constexpr variable’s initializer must be a constant expression, so f1<S> must clearly be instantiated (and evaluated) at the point of the variable’s definition.