My understanding of function templates has always been: if they contain invalid
C++ and you don’t instanciate them, your project will compile fine.
However, the following code:
#include <cstdio>
#include <utility>
template <typename T>
void contains_compile_time_error(T&& t) {
int j = nullptr;
}
int main() {}
Compiles with:
x86-64 gcc 11.2and flag-std=c++20;x64 msvc v19.31and flag/std:c++20;
and does not with x86-64 clang 14.0.0 (with flags -std=c++{11,14,17,20}):
error: cannot initialize a variable of type 'int' with an rvalue of type 'std::nullptr_t'
int j = nullptr;
^ ~~~~~~~
1 error generated.
Even more confusing to me, the following code:
#include <cstdio>
#include <utility>
namespace ns {
struct S {};
template <typename T>
void apply(T&& t) {
//print(std::forward<T>(t));
ns::print(std::forward<T>(t));
}
void print(S const&) { std::puts("S"); }
} // namespace ns
int main() {}
Does not compile with:
x86-64 gcc 11.2and flag-std=c++20;x64 msvc v19.31and flag/std:c++20;x86-64 clang 14.0.0and flags-std=c++{11,14,17,20};
(all of them complaining that print is not a member of 'ns') but does compile with x64 msvc v19.31 /std:c++17.
If I call unqualified print, then the code compiles with all the above compilers.
So, my questions are:
- is my understanding of function templates wrong?
- why do the above compilers behave differently with the code snippets I posted?
Edit 0: as per Frank’s comment, x64 msvc v19.31 /std:c++17 /permissive- fails to compile function template apply where I call the qualified ns::print.
>Solution :
My understanding of function templates has always been: if they contain invalid C++ and you don’t instanciate them, your project will compile fine.
But what does "invalid C++" mean? You can have syntactically valid C++ that is still invalid semantically, and the semantics of C++ are highly contextual.
So there’s multiple levels of "valid" that can be checked at different times during compilation, based on the available information.
There are three types of lookups involved here.
- Qualified lookups, such as
ns::print - Unqualified lookups not involving template parameters, which you’d get if you tried
print(S{}); - Unqualified lookups involving template parameters, such as
print(std::forward<T>(t));
Lookups are deferred for the third category, but are still performed the same as non-template functions for the other two.
Note that the code still needs to be syntactically valid, even in the case of deferred lookups, hence why typename needs to be added when doing a dependent lookup of a type.
why do the above compilers behave differently with the code snippets I posted?
As far as int j = nullptr is concerned: The program is technically ill-formed, but the diagnostics is optional. I believe this is because it’s not possible to guarantee that every single ill-formed statement can be caught in non-instantiated templates. So the code is broken, but GCC is not breaking compliance by letting it through.
For example, if S only had private constructors, then print(std::forward<T>(t)) would be flaggable as being broken since there is no possibly T that would make it valid. However, requiring compilers to be smart enough to determine this in all cases would be kind of mean.
And as for MSVC allowing ns::print: That compiler is not compliant by default on this (and other) aspect of the standard. You need to use the /permissive- compiler flag to enforce compliance.