Your program is invalid (ill-formed). However, no diagnostic is required.
An issue of the same general nature can be demonstrated by a more simple
example
class C;
template <typename T = int> void foo() {
C c; // Incomplete type error? Or not?
}
int main() {
foo();
}
class C {};
Note that the above code is also quietly accepted by GCC, but rejected
by MSVC and Clang.
Briefly and informally, the rule it violates says the following: if the
interpretation of your template changes depending on its point of
instantiation, the program is ill-formed. In a more focused and formal
form: if a reference to an non-dependent name from an imaginary
instantiation that immediately follows the definition is invalid, the
program is ill-formed.
A complete set of formal requirements can be found in [temp.res]:
https://timsong-cpp.github.io/cppwp/n4659/temp.res#8.3
https://timsong-cpp.github.io/cppwp/n4659/temp.res#8.4
https://timsong-cpp.github.io/cppwp/n4659/temp.res#temp.point-8
The first two links deal with non-dependent names, while the third one
applies to dependent names. But the general idea is the same: if the
meaning of a template specialization depends on its point of
instantiation, the program is ill-formed. If the meaning of a template
specialization changes as you move it up and down in the translation
unit, the program is ill-formed. Yet, no diagnostic is required.
This is basically it.
But there's another detail at play here. The freedom provided by "no
diagnostic is required" supplies the implementations with quite a bit of
leeway in choosing the point of instantiation. The obvious next logical
step is the resolution of DR#993
https://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#993
It allows implementations to stop worrying about choosing the proper
point of instantiation completely. It allows them to simply push all
instantiations to the very end of the translation unit and be done with it.
This is an opportunity GCC immediately took advantage of. GCC
instantiates templates at the very end of the TU and, expectedly,
interprets their content from that vantage point. Which is why GCC
reports errors neither in your example nor in mine. Meanwhile, Clang and
MSVC still stick to the "early instantiation" approach, which allows
them to see the problems in our examples.
--
Best regards,
Andrey