Another requires-clause syntax pitfall

In my CppCon 2018 talk “Concepts As She Is Spoke,” I presented some pitfalls in requires-clause syntax, such as (Godbolt):

template<class T>
concept HasHash = requires (const T& t) {
    t.hash() -> std::integral;

struct X { int hash() const; };

On the cpplang Slack today, John Eivind Helset and Eugenio Bargiacchi (inadvertently) showed me a new trap I hadn’t seen before (Godbolt):

template<class T>
concept HasAButNotB = requires (T t) {
    !requires { t.b(); };

struct S1 {};
struct S2 { void a(); };
struct S3 { void b(); };
struct S4 { void a(); void b(); };

static_assert(!HasAButNotB<S1>);  // as expected
static_assert(HasAButNotB<S2>);   // as expected
static_assert(!HasAButNotB<S3>);  // as expected
static_assert(HasAButNotB<S4>);   // ...oops!

Interestingly, most compilers not only warn but error out on

template<class T>
concept HasX = requires (T t) {
    requires { t.x(); };

but as soon as you put operator! in front of it, every compiler accepts it without complaint.

As usual, the problem is that we’re checking for the well-formedness of the expression !requires { t.b(); }, not the truth value of that expression.

Each different vendor diagnoses a slightly different subset of possible user errors in this area:

requirement Did you mean it? Is it valid C++20? GCC Clang MSVC
t.b(); yes yes OK OK OK
t.b() -> X; no yes OK OK OK
t.b() -> std::X; no yes syntax error OK warning C5207 at declaration
requires t.b(); probably not yes hard error if t.b() exists OK hard error if t.b() exists
requires { t.b(); }; no no? syntax error -Wrequires-expression at declaration syntax error
!requires t.b(); no no syntax error syntax error syntax error
!requires { t.b(); }; no yes OK OK OK

See also:

Posted 2021-06-09