constexpr if and static_assert
This is talking about a well-established rule for templates - the same rule that allows compilers to diagnose template<class> void f() { return 1; }
. [temp.res]/8 with the new change bolded:
The program is ill-formed, no diagnostic required, if:
- no valid specialization can be generated for a template or a substatement of a
constexpr if
statement ([stmt.if]) within a template and the template is not instantiated, or- [...]
No valid specialization can be generated for a template containing static_assert
whose condition is nondependent and evaluates to false
, so the program is ill-formed NDR.
static_assert
s with a dependent condition that can evaluate to true
for at least one type are not affected.
Edit: I'm keeping this self-answer with examples and more detailed explanations of the misunderstandings that lead to this questions. The short answer by T.C. is strictly enough.
After rereading the proposal and on static_assert
in the current draft, and I conclude that my worries were misguided. First of all, the emphasis here should be on template definition.
ill-formed; no diagnostic required for template definition
If a template is instantiated, any static_assert
fire as expected. This presumably plays well with the statement I quoted:
... a discarded statement is not instantiated.
This is a bit vague to me, but I conclude that it means that templates occurring in the discarded statement will not be instantiated. Other code
however must be syntactically valid. A static_assert(F)
, [where F is false, either literally or a constexpr value] inside a discarded if constexpr
clause will thus still 'bite' when the template containing the static_assert
is instantiated. Or (not required, at the mercy of the compiler) already at declaration if it's known to always be false.
Examples: (live demo)
#include <type_traits>
template< typename T>
constexpr void some_library_foo(){
static_assert(std::is_same<T,int>::value);
}
template< typename T>
constexpr void other_library_bar(){
static_assert(std::is_same<T,float>::value);
}
template< typename T>
constexpr void buzz(){
// This template is ill-formed, (invalid) no diagnostic required,
// since there are no T which could make it valid. (As also mentioned
// in the answer by T.C.).
// That also means that neither of these are required to fire, but
// clang does (and very likely all compilers for similar cases), at
// least when buzz is instantiated.
static_assert(! std::is_same<T,T>::value);
static_assert(false); // does fire already at declaration
// with latest version of clang
}
template<class T, bool IntCase>
void g() {
if constexpr (IntCase){
some_library_foo<T>();
// Both two static asserts will fire even though within if constexpr:
static_assert(!IntCase) ; // ill-formed diagnostic required if
// IntCase is true
static_assert(IntCase) ; // ill-formed diagnostic required if
// IntCase is false
// However, don't do this:
static_assert(false) ; // ill-formed, no diagnostic required,
// for the same reasons as with buzz().
} else {
other_library_bar<T>();
}
}
int main(){
g<int,true>();
g<float,false>();
//g<int,false>(); // ill-formed, diagnostic required
//g<float,true>(); // ill-formed, diagnostic required
}
The standard text on static_assert
is remarkably short. In standardese, it's a way to make the program ill-formed with diagnostic (as @immibis also pointed out):
7.6 ... If the value of the expression when so converted is true, the declaration has no effect. Otherwise, the program is ill-formed, and the resulting diagnostic message (1.4) shall include the text of the string-literal, if one is supplied ...
C++20 makes static_assert
in the else
branch of if constexpr
much shorter now, because it allows template lambda parameters. So to avoid the ill-formed case, we can now define a lambda with a bool
template non-type parameter that we use to trigger the static_assert
. We immediately invoke the lambda with ()
, but since the lambda won't be instantiated if its else
branch is not taken, the assertion will not trigger unless that else
is actually taken:
template<typename T>
void g()
{
if constexpr (case_1)
// ...
else if constexpr (case_2)
// ...
else
[]<bool flag = false>()
{static_assert(flag, "no match");}();
}
Your self-answer and possibly the one by T.C. are not quite correct.
First of all, the sentence "Both two static asserts will fire even though within if constexpr
" is not correct. They won't because the if constexpr
condition depends on a template parameter.
You can see that if you comment out the static_assert(false)
statements and the definition of buzz()
in your example code: static_assert(!IntCase)
won't fire and it will compile.
Furthermore, things like AlwaysFalse<T>::value
or ! std::is_same_v<T, T>
are allowed (and have no effect) inside a discarded constexpr if
, even if there's no T
for which they evaluate to true.
I think that "no valid specialization can be generated" is bad wording in the standard (unless cppreference is wrong; then T.C. would be right). It should say "could be generated", with further clarification of what is meant by "could".
This is related to the question whether AlwaysFalse<T>::value
and ! std::is_same_v<T, T>
are equivalent in this context (which is what the comments to this answer are about).
I would argue that they are, since it's "can" and not "could" and both are false for all types at the point of their instantiation.
The crucial difference between std::is_same
and the non-standard wrapper here is that the latter could theoretically be specialized (thanks, cigien, for pointing this out and providing the link).
The question whether ill-formed NDR or not also crucially depends on whether the template is instantiated or not, just to make that entirely clear.