13

When we want to use a static_assert in a if constexpr we must make the condition dependent on some template parameter. Interestingly, gcc and clang disagree when the code is wrapped in a lambda.

The following code compiles with gcc, but clang triggers the assert, even if the if constexpr can't be true.

#include <utility>

template<typename T> constexpr std::false_type False;

template<typename T>
void foo() {

    auto f = [](auto x) {
        constexpr int val = decltype(x)::value;
        if constexpr(val < 0) {
            static_assert(False<T>, "AAA");
        }
    };

    f(std::integral_constant<int, 1>{});
}

int main() {
    foo<int>();
}

Live example here.

It can easily be fixed by substituting False<T> by False<decltype(x)>.

So the question is: which compiler is right? I'd assume that gcc is correct because the condition in the static_assert is dependent on T, but I'm not sure.

florestan
  • 4,405
  • 2
  • 14
  • 28
  • This asks kind of the same question but coming from the opposite direction: https://stackoverflow.com/questions/59393908/if-constexpr-why-is-discarded-statement-fully-checked. – NathanOliver Jan 08 '20 at 14:10
  • gcc optimizes the lambda out as you aren't doing anything with it. The call f(...) is optimized out as well of course. – mfnx Jan 08 '20 at 14:12
  • @NathanOlivier nope. However, write auto foo() { ... return f (... ) ; } and it will trigger the static_assert. – mfnx Jan 08 '20 at 14:13
  • 1
    @mfnx [Can't reproduce](https://godbolt.org/z/3eWdMJ). Can you share an example? – NathanOliver Jan 08 '20 at 14:14
  • 2
    I would say both are right (ill formed NDR): `static_assert(False, "AAA");` is equivalent to `static_assert(false, "AAA");` inside the lambda. – Jarod42 Jan 08 '20 at 14:14
  • @mfnx Can't reproduce, too. – florestan Jan 08 '20 at 14:15
  • @NathanOliver you could also compile with -O0 and you would see the same. Example returning f(...): https://wandbox.org/permlink/pgB8pfzv3Dfaqhjo – mfnx Jan 08 '20 at 14:15
  • @Jarod42, this sounds reasonable. Do you have a reference in the standard? – florestan Jan 08 '20 at 14:16
  • 2
    @mfnx You've changed the value of the constant. Using the OP's example where the constant is `f(std::integral_constant{});` Wandbox doesn't trigger the assert: https://wandbox.org/permlink/UFYAmYwtt1ptsndr – NathanOliver Jan 08 '20 at 14:23
  • 1
    @NathanOliver Yes you are right, sorry for the noise. Seems gcc is just right as that code in the constexpr should be discarded if the constant >= 0; – mfnx Jan 08 '20 at 14:26
  • GCC is correct after [P0588R1](https://wg21.link/p0588r1), IIUC (see [P0588R0](https://wg21.link/p0588r0) for its background). – cpplearner Jan 08 '20 at 18:54

2 Answers2

13

The usual rule here is [temp.res]/8:

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 within a template and the template is not instantiated

Once you instantiate foo<T>, the static_assert you have is no longer dependent. It becomes static_assert(false) - for all possible instantiations of the call operator of the generic lambda f. That's ill-formed, no diagnostic required. Clang diagnoses, gcc doesn't. Both are correct.

Note that it doesn't matter that the static_assert here is discarded.

It can easily be fixed by substituting False<T> by False<decltype(x)>.

This keeps the static_assert dependent within the generic lambda, and now we get into a state where there could hypothetically be a valid specialization, so we're no longer ill-formed, ndr.

Barry
  • 286,269
  • 29
  • 621
  • 977
1

From [stmt.if]/2 (emphasis mine)

If the if statement is of the form if constexpr, the value of the condition shall be a contextually converted constant expression of type bool; this form is called a constexpr if statement. If the value of the converted condition is false, the first substatement is a discarded statement, otherwise the second substatement, if present, is a discarded statement. 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.

Reading that one would think the static assert would be dropped, but this is not the case.

The static assert is triggered in the first phase of the template because the compiler know it's always false.

From [temp.res]/8 (emphasis mine)

The validity of a template may be checked prior to any instantiation. [ Note: Knowing which names are type names allows the syntax of every template to be checked in this way. — end note ] The program is ill-formed, no diagnostic required, if:

  • (8.1) no valid specialization can be generated for a template or a substatement of a constexpr if statement within a template and the template is not instantiated, or

[...]

Yes indeed, your False<T> depends on T. The problem is that a generic lambda is itself a template, and False<T> is not dependent on any template parameter of the lambda.

For a T that False<T> is false, the static assert will always be false, no matter which template argument is sent to the lambda.

The compiler can see that for any instantiation of the template operator(), the static assert will always trigger for the current T. Hence the compiler error.

A solution for this would be to depend on x:

template<typename T>
void foo() {

    auto f = [](auto x) {
        if constexpr(x < 0) {
            static_assert(False<decltype(x)>, "AAA");
        }
    };

    f(std::integral_constant<int, 1>{});
}

Live example

Guillaume Racicot
  • 39,621
  • 9
  • 77
  • 141