How does a failed static_assert work in an if constexpr (false) block?
Asked Answered
M

8

75

P0292R1 constexpr if has been included, on track for C++17. It seems useful (and can replace use of SFINAE), but a comment regarding static_assert being ill-formed, no diagnostic required in the false branch scares me:

Disarming static_assert declarations in the non-taken branch of a
constexpr if is not proposed.

void f() {
  if constexpr (false)
    static_assert(false);   // ill-formed
}

template<class T>
void g() {
  if constexpr (false)
    static_assert(false);   // ill-formed; no 
               // diagnostic required for template definition
}

I take it that it's completely forbidden to use static_assert inside constexpr if (at least the false / non-taken branch, but that in practice means it's not a safe or useful thing to do).

How does this come about from the standard text? I find no mentioning of static_assert in the proposal wording, and C++14 constexpr functions do allow static_assert (details at cppreference: constexpr).

Is it hiding in this new sentence (after 6.4.1) ? :

When a constexpr if statement appears in a templated entity, during an instantiation of the enclosing template or generic lambda, a discarded statement is not instantiated.

From there on, I assume that it is also forbidden, no diagnostic required, to call other constexpr (template) functions which somewhere down the call graph may call static_assert.

Bottom line:

If my understanding is correct, doesn't that put a quite hard limit on the safety and usefulness of constexpr if as we would have to know (from documentation or code inspection) about any use of static_assert? Are my worries misplaced?

Update:

This code compiles without warning (clang head 3.9.0) but is to my understanding ill-formed, no diagnostic required. Valid or not?

template< typename T>
constexpr void other_library_foo(){
    static_assert(std::is_same<T,int>::value);
}

template<class T>
void g() {
  if constexpr (false)
    other_library_foo<T>(); 
}

int main(){
    g<float>();
    g<int>();
}
Menchaca answered 11/7, 2016 at 10:38 Comment(12)
It's ill-formed because the condition is false. Not because it's inside a constexpr if...Sheave
@immibis. It's clear that this is all about the non-taken branch, so I don't understand what you mean specifically. Care to elaborate and interpret in terms of the bottom line question?Menchaca
clang already implemented if constexpr. If you have any doubt, why not try it on your own?Domingo
@cpplearner, Done that, but it does not add add much. The question is about what the standard say and its implications.Menchaca
@JohanLundberg Well, obviously it's ill-formed if it's inside the taken branch.Sheave
Currently there's no standard or draft standard that contains the wording for if constexpr, and P0292R2, the paper that got accepted, is also not publicly available yet.Domingo
@immibis huh? Could you elaborate in an answer and clarify both obvious and non obvious parts?Menchaca
@JohanLundberg A program containing static_assert(false); is ill-formed. But constexpr if(false) removes the code inside it. So the only thing that needs to be clarified is: when you combine both, does the static_assert(false); make the program ill-formed, or does the constexpr if(false) remove it before the compiler checks the static_assert?Sheave
@immibis: "But constexpr if(false) removes the code inside it." That's the thing: it doesn't remove the code inside the not taken branch. It makes them into discarded statements. There's a difference.Mycobacterium
@immibis, I did not consider that the deliberate compilation fail generated by static_assert(false) is via 'ill-formed', but it makes sense. What worries me is the unclarity regarding if it's active or not in the discared statements, and especially with 'no diagnostic required', and the bottom line question on how we can safely use constexpr with other peoples code.Menchaca
#define false ([](auto e) { return e; }(false)) might help.Introduce
There is now a paper about this problem: open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2593r0.htmlUmbrian
D
46

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_asserts with a dependent condition that can evaluate to true for at least one type are not affected.

Decorum answered 11/7, 2016 at 23:38 Comment(5)
Thank you especially for the draft link. It makes sense as evaluating invalid templates can be arbitrarily hard.Menchaca
Thanks again. Accepted. It's worth noting that the lines you quoted are in the original proposal I linked to, but I missed them.Menchaca
Helper to make false template parameter dependet:template < typename > constexpr bool false_c = false;Fireboard
However, and I don't know if that is a compiler bug or what is it, but, I had a function which different if constexpr statements that does depend on the template, and a static_assert(false) in the last else case. Since the static_assert won't be evaluated unless the other if constexpr statements fails, and thus, since the evaluation of the static_assert depends (indirectly) on the type, I think the code shouldn't be ill-formed, because there's "possible inputs" that doesn't trigger the static_assert.Otti
@Peregring-lk It is ill-formed NDR, because "substatements of a constexpr if statements" are also affected. It is not (only) about the constexpr if statement as a whole.Anthracite
P
37

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");}();
}
Prisage answered 14/10, 2020 at 13:24 Comment(6)
In C++17 one can define a template function outside: template<bool flag = false> void static_no_match() { static_assert(flag, "no match"); } and then use static_no_match() in the else branch.Ricer
In C++17 we can just make the condition dependent on the template parameter, like static_assert(!sizeof(T), "no match");. See it liveTribunate
"else" branch looks ugly with this lambda.Hyohyoid
@Hyohyoid You should be able to macro it. Like STATIC_FAIL("no match");Prisage
I’m not at all sure that this evades the IFNDR rule: it’s still the case that no valid specialization of the substatement exists, although one would have to reason out whether the validity of the chosen specialization of the lambda’s operator() counted for that purpose.Splatter
This answer is referenced in the P2593 committee paper where it's described as "terrible and wrong". That paper has been accepted into C++23 and will allow static_assert(false).Peccavi
M
10

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 ...

Menchaca answered 11/7, 2016 at 22:6 Comment(2)
A few years passed, do you happen to know a better solution? static_assert(IntCase) is very inconvenient for complex nest if-else. I really wish to call static_assert(false) within some else.Influent
The phrase "ill-formated" is ill-formed. (No pun intended)Nystatin
R
8

The most concise way I've come across to work-around this (at least in current compilers) is to use !sizeof(T*) for the condition, detailed by Raymond Chen here. It's a little weird, and doesn't technically get around the ill-formed problem, but at least it's short and doesn't require including or defining anything. A small comment explaining it may assist readers:

template<class T>
void g() {
  if constexpr (can_use_it_v<T>) {
    // do stuff
  } else {
    // can't use 'false' -- expression has to depend on a template parameter
    static_assert(!sizeof(T*), "T is not supported");
  }
}

The point of using T* is to still give the proper error for incomplete types.

I also came across this discussion in the old isocpp mailing list which may add to this discussion. Someone there brings up the interesting point that doing this kind of conditional static_assert is not always the best idea, since it cannot be used to SFINAE-away overloads, which is sometimes relevant.

Romanist answered 10/11, 2021 at 23:29 Comment(5)
It’s still IFNDR to have a branch of a constexpr if that’s invalid for all instantiations.Splatter
Is it an ill-formed expression that all the compilers chose to not implement? seems to work as expected on all 3 major ones. I believe by making it type-dependent, it delays the static_assert until after if constexpr is resolved.Romanist
That’s what “no diagnostic required” means—the implementation might or might not do enough analysis to determine that it’s always invalid. “Delaying” the static_assert may be a thing that can happen in (today’s) real compilers, but it doesn’t mean anything formally at all.Splatter
Thanks for clarifying my understanding! You're saying that some day, a compiler may do a value range analysis earlier and see that it is always false. Could go either way I guess, with either the implementers wanting to do more analysis (like they do in some of the UB cases), or the committee specifying that this is okay (or an alternative). The desire to write asserts like this seem to come up reasonably often.Romanist
The committee has considered providing an idiom here. They’ve also informally considered restricting the scope of the problem, because technically a compiler can also arbitrarily miscompile a program that does this.Splatter
R
4

This has been found to be a defect, CWG 2518. Static asserts now are ignored in template declarations, so now are delayed until instantiation. Failing static asserts are no longer ill-formed no diagnostic required during template resolution.

It is being applied to all C++ modes in clang and GCC 13.

Romanist answered 22/6, 2023 at 18:59 Comment(0)
M
1

A comma expression can make the static_assert condition dependent on template arguments:

  #include <type_traits>

  template<typename T>
  constexpr int func(T x)
  {
    if constexpr(std::is_same_v<T, int>){
      return x;
    } else {
      static_assert((sizeof(T), false), "Bad template argument");
      return 0;
    }
   }

Or you can make a dependent expression which is always false:

      static_assert(sizeof(T) == 0, "Bad template argument");
Mafala answered 12/10, 2023 at 18:2 Comment(0)
A
0

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.

Anthracite answered 21/3, 2020 at 18:53 Comment(10)
Would you care to explain your point more? Most of the answers in the question you link to conclude that a non std wrapper is required. Such as https://mcmap.net/q/270677/-how-to-assert-that-a-constexpr-if-else-clause-never-happenMenchaca
No, as far as I see they only ever speak of a "type-dependent" expression, i.e. an expression that depends on T.Anthracite
It works pretty nice, without warning on gcc or clang. wandbox.org/permlink/b7DMBGyaFj7V2Nc7Parisparish
This is not correct. It would be fine if specializations of is_same_v could exist, but "The behavior of a program that adds specializations for is_same or is_same_v (since C++17) is undefined.".Amphipod
@Amphipod It's not about adding specializations for is_same. The standard one serves this purpose just fine. (So would any other expression that always yields false but depends on T.)Anthracite
Actually, it's ifndr if it could only yield false. There has to be at least the possibility of it being true, which in the case of std::is_same<T,T> its not.Amphipod
@Amphipod Do you have a source for that? (There's some more discussion in the accepted answer to the thread I linked to in my answer. Maybe it would fit better there and I would adjust my wording accordingly or delete my post.)Anthracite
@Amphipod I think I understand your point of view now. I would still argue that "no valid specialization could be generated" for the (outer) template, whether specializations for the template in the expression can exist or not (because specializations afterwards or in another place have no effect on the instantiation; you would need a (never used) specialization of a hypothetical AlwaysFalse which actually isn't false to conform to your reading, wouldn't you?). I edited my answer to clarify that.Anthracite
Hmm, see this answerAmphipod
@Amphipod Thanks. I would say, though, that we unfortunately still don't have a reference to the standard which entirely clarifies this (if the possibility is enough or not). I edited my answer again to elaborate a bit more. Furthermore, you could argue that without an actual specialization yielding true there's not even the possibility.Anthracite
M
-4

My solution is:

if constexpr (is_same_v<T,int>)
  // ...
else if constexpr (is_same_v<T,float>)
  // ...
else
  static_assert(std::is_same_v<T, void> && !std::is_same_v<T, void>, "Unsupported element type.");
Mastership answered 25/11, 2022 at 6:40 Comment(1)
This is "ill formed, no diagnostics required". Read other answers why.Terminate

© 2022 - 2024 — McMap. All rights reserved.