69

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>();
}
Johan Lundberg
  • 26,184
  • 12
  • 71
  • 97
  • 1
    It's ill-formed because the condition is false. Not because it's inside a constexpr if... – user253751 Jul 11 '16 at 10:42
  • 3
    @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? – Johan Lundberg Jul 11 '16 at 10:59
  • clang already implemented `if constexpr`. If you have any doubt, why not [try it on your own](http://melpon.org/wandbox/permlink/8AmTzaSIIUi4M1kY)? – cpplearner Jul 11 '16 at 11:24
  • 3
    @cpplearner, Done that, but it does not add add much. The question is about what the *standard* say and its implications. – Johan Lundberg Jul 11 '16 at 11:51
  • @JohanLundberg Well, *obviously* it's ill-formed if it's inside the taken branch. – user253751 Jul 11 '16 at 12:22
  • 1
    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. – cpplearner Jul 11 '16 at 12:36
  • @immibis huh? Could you elaborate in an answer and clarify both obvious and non obvious parts? – Johan Lundberg Jul 11 '16 at 12:53
  • @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`? – user253751 Jul 11 '16 at 13:37
  • 1
    @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. – Nicol Bolas Jul 11 '16 at 14:05
  • @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. – Johan Lundberg Jul 11 '16 at 14:18
  • `#define false ([](auto e) { return e; }(false))` might help. – Vladimir Reshetnikov Feb 26 '18 at 20:18
  • 1
    There is now a paper about this problem: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2593r0.html – pathfinderelite Aug 21 '22 at 12:35

7 Answers7

44

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.

T.C.
  • 133,968
  • 17
  • 288
  • 421
  • Thank you especially for the draft link. It makes sense as evaluating invalid templates can be arbitrarily hard. – Johan Lundberg Jul 12 '16 at 06:34
  • Thanks again. Accepted. It's worth noting that the lines you quoted are in the original proposal I linked to, but I missed them. – Johan Lundberg Jul 12 '16 at 19:42
  • Helper to make false template parameter dependet:template < typename > constexpr bool false_c = false; – Benjamin Buch Jun 16 '17 at 20:47
  • 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`. – ABu Oct 27 '17 at 03:58
  • @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. – philipp2100 Sep 16 '20 at 04:46
34

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");}();
}
Nikos C.
  • 50,738
  • 9
  • 71
  • 96
  • 7
    In C++17 one can define a template function outside: `template void static_no_match() { static_assert(flag, "no match"); }` and then use `static_no_match()` in the `else` branch. – cxxl Nov 27 '20 at 08:35
  • 16
    In C++17 we can just make the condition dependent on the template parameter, like `static_assert(!sizeof(T), "no match");`. [See it live](https://coliru.stacked-crooked.com/a/c44a266099961a6c) – Ruslan May 24 '21 at 18:15
  • 4
    "else" branch looks ugly with this lambda. – korst1k Nov 05 '21 at 17:30
  • 1
    @korst1k You should be able to macro it. Like `STATIC_FAIL("no match");` – Nikos C. Nov 05 '21 at 20:22
  • 3
    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. – Davis Herring Nov 11 '21 at 04:03
  • 2
    This answer is referenced in the [P2593 committee paper](https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2593r0.html) where it's described as "terrible and wrong". That paper has been accepted into C++23 and will allow `static_assert(false)`. – interjay Feb 15 '23 at 09:51
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 ...

florestan
  • 4,405
  • 2
  • 14
  • 28
Johan Lundberg
  • 26,184
  • 12
  • 71
  • 97
  • 3
    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`. – javaLover Mar 05 '19 at 08:13
  • The phrase "ill-formated" is ill-formed. (No pun intended) – L. F. May 28 '19 at 12:19
4

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.

golvok
  • 1,015
  • 12
  • 25
  • 4
    It’s still IFNDR to have a branch of a constexpr if that’s invalid for all instantiations. – Davis Herring Nov 11 '21 at 03:59
  • Is it an ill-formed expression that all the compilers chose to not implement? [seems to work as expected on all 3 major ones](https://godbolt.org/z/YcEM3vbbY). I believe by making it type-dependent, it delays the `static_assert` until after `if constexpr` is resolved. – golvok Nov 11 '21 at 18:02
  • 2
    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. – Davis Herring Nov 11 '21 at 18:08
  • 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. – golvok Nov 11 '21 at 20:49
  • 2
    The committee has [considered](http://open-std.org/JTC1/SC22/WG21/docs/papers/2019/p1830r1.pdf) 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. – Davis Herring Nov 11 '21 at 21:24
2

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.

golvok
  • 1,015
  • 12
  • 25
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.

philipp2100
  • 162
  • 1
  • 11
  • 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://stackoverflow.com/a/53945555/1149664 – Johan Lundberg Mar 21 '20 at 20:17
  • No, as far as I see they only ever speak of a "type-dependent" expression, i.e. an expression that depends on T. – philipp2100 Mar 23 '20 at 16:50
  • It works pretty nice, without warning on gcc or clang. https://wandbox.org/permlink/b7DMBGyaFj7V2Nc7 – Martin Morterol Jun 19 '20 at 05:28
  • 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."*. – cigien Sep 07 '20 at 13:46
  • @cigien 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`.) – philipp2100 Sep 08 '20 at 14:35
  • 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` its not. – cigien Sep 08 '20 at 16:25
  • @cigien 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.) – philipp2100 Sep 09 '20 at 05:08
  • @cigien 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. – philipp2100 Sep 09 '20 at 06:12
  • Hmm, see this [answer](https://stackoverflow.com/a/57812126/8372853) – cigien Sep 09 '20 at 13:54
  • @cigien 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. – philipp2100 Sep 16 '20 at 06:15
-2

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.");