From my understanding, dependent name lookup doesn't take place until template instantiation, and template invocations in a discarded if-constexpr statement are not instantiated. As such, I would expect a template or generic lambda that is ill-formed, due to missing dependent names, to not produce compilation errors as long as it is only used in discarded if-constexpr statements. This appears to be the case in some instances. For example, take:
struct Struct {};
Struct s;
template<typename T>
void foo(T& s) {
s.non_existing_member = 0;
}
struct A {
template<typename T>
void operator()(T& s) { // note 'void' return type
s.non_existing_member = 0;
}
};
struct B {
template<typename T>
auto operator()(T& s) { // note 'auto' return type
s.non_existing_member = 0;
}
};
As expected, these do not produce compilation errors:
if constexpr (false) {
foo(s);
A{}(s);
}
[](auto& s) {
if constexpr (false) {
s.non_existing_member = 0;
}
}(s);
However, these do, complaining about the missing member:
if constexpr (false) {
auto bar = [](auto& s) {
s.non_existing_member = 0;
};
// error: no member named 'non_existing_member' in 'Struct'
bar(s); // note: in instantiation of function template specialization 'main()::(anonymous class)::operator()<Struct>'
B{}(s); // note: in instantiation of function template specialization 'B::operator()<Struct>' requested here
}
I don't quite understand what is different about the above two cases. I get similar errors referencing dependent type names, e.g. typename T::Type
.
Rereading the docs (thanks @Jarod42):
Outside a template, a discarded statement is fully checked. if constexpr is not a substitute for the #if preprocessing directive.
If a constexpr if statement appears inside a templated entity, and if condition is not value-dependent after instantiation, the discarded statement is not instantiated when the enclosing template is instantiated.
I would actually expect foo(s);
and A{}(s);
to fail compilation as well. They don't, though, with neither of the latest of clang, gcc, or MSVC. I would also expect the following to work, which it does. Full example.
template<typename T>
void baz(T& s) {
if constexpr (false) {
s.non_existing_member = 0;
}
}
struct C {
template<typename T>
auto operator()(T& s) {
if constexpr (false) {
s.non_existing_member = 0;
}
}
};
int main() {
baz(s);
C{}(s);
}
Being explicit about the return type of the generic lambda seems to work (allow compilation) with gcc and MSVC, but not with clang:
auto bar = [](auto& s) -> void {
s.non_existing_member = 0;
};
if constexpr (false)
only prevents instantiation of code when there is a surrounding template. Otherwise, it works mostly like if (false)
.
The specifics are in [stmt.if] p2:
During the instantiation of an enclosing templated entity ([temp.pre]), if the condition is not value-dependent after its instantiation, the discarded substatement (if any) is not instantiated.
However, another important paragraph is [basic.def.odr] p12:
Every program shall contain at least one definition of every function or variable that is odr-used in that program outside of a discarded statement; no diagnostic required.
As a consequence, odr-use within if constexpr (false)
doesn't require instantiation to happen, and in general, templates are only instantiated when needed.
if constexpr (false) { foo(s); A{}(s); }
Here, foo(s)
and A{}(s)
are located outside any templated entity.
However, the calls to foo(s)
and A{}(s)
are odr-use within a discarded statement, so they don't result in instantiation of those templates.
Note that foo
and A::operator()
return void
; they don't have deduced return types, so an instantiation isn't needed to check the validity of the program.
if constexpr (false) { auto bar = [](auto& s) { s.non_existing_member = 0; }; // error: no member named 'non_existing_member' in 'Struct' bar(s); // note: in instantiation of function template specialization 'main()::(anonymous class)::operator()<Struct>' B{}(s); // note: in instantiation of function template specialization 'B::operator()<Struct>' requested here }
Once again, if constexpr
acts like a simple if
because this code is located inside main
.
Once again, all odr-use within this discarded statement is irrelevant.
As stated in [temp.inst] p5, function templates aren't instantiated for no reason:
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.
A definition isn't required to exist (see [basic.def.odr] p12) and existence of a definition doesn't affect semantics because no constant evaluation takes place (see [temp.inst] p8).
bar
and B::operator()
have a deduced return type, and it looks like compilers are using this as an excuse to eagerly instantiate these templates despite not being allowed to.
However, that is a bug.
[](auto& s) { if constexpr (false) { s.non_existing_member = 0; } }(s); baz(s); C{}(s);
Here, the code inside the discarded statement is not instantiated because there actually is a surrounding templated entity.
Namely, the call operator of this generic lambda is implicitly a function template, just like C::operator()
.
When the call operator is instantiated, s.non_existing_member
is not instantiated, so it doesn't cause an error.