Home > Back-end >  Why do different compilers behave differently with these requires expressions?
Why do different compilers behave differently with these requires expressions?

Time:12-28

Why do some of these implementations of a "Oneable" concept using requires expressions in C 20 not compile on certain compilers?

// `Oneable` implemented using `requires` with a simple assignment expression

// clang, gcc, and msvc all compile

template <class T>
concept Oneable = requires(T n) { n = 1; };

static_assert(Oneable<int>);
static_assert(!Oneable<int*>);
// `Oneable` implemented using `requires` with an assignment statement inside
// the body of a lambda expression

// clang and msvc compile
// gcc gives a compiler error on instantiating Oneable<T*>

template <class T>
concept Oneable = requires { []() { T n = 1; }; };

static_assert(Oneable<int>);
static_assert(!Oneable<int*>);
// `Oneable` implemented using `requires` with an instantiation of a struct

// gcc and msvc compile
// clang gives a compiler error on instantiating Oneable<int*>

template <class T>
struct S { T n = 1; };

template <class T>
concept Oneable = requires { S<T>{}; };

static_assert(Oneable<int>);
static_assert(!Oneable<int*>);

gcc's output for the second snippet:

<source>: In lambda function:
<source>:8:43: error: invalid conversion from 'int' to 'int*' [-fpermissive]
    8 | concept Oneable = requires { []() { T n = 1; }; };
      |                                           ^
      |                                           |
      |                         

clang's output for the third snippet:

<source>:8:18: error: cannot initialize a member subobject of type 'int *' with an rvalue of type 'int'
struct S { T n = 1; };
                 ^
<source>:11:35: note: in instantiation of default member initializer 'S<int *>::n' requested here
concept Oneable = requires { S<T>{}; };
                                  ^
<source>:11:30: note: in instantiation of requirement here
concept Oneable = requires { S<T>{}; };
                             ^~~~~~
<source>:11:19: note: while substituting template arguments into constraint expression here
concept Oneable = requires { S<T>{}; };
                  ^~~~~~~~~~~~~~~~~~~~
<source>:15:18: note: while checking the satisfaction of concept 'Oneable<int *>' requested here
    std::cout << Oneable<int*> << '\n';
                 ^~~~~~~~~~~~~

The compilers used are x86-64 gcc 12.2, x86-64 clang 15.0.0, and x64 msvc 19.33.

No flags are given other than those required to set the language standard.

CodePudding user response:

The issue here has to do with immediate context. The technology at play here is SFINAE - substitution failure is not an error. But this is only not an error in what's called the immediate context of the substitution - a term which is basically not really well defined, unfortunately.

Basically, though, the actual signature of a function template is the immediate context, but the body of a function template is not. Anything that goes wrong in a function body is a hard compiler error, not a gracefully-checkable one. So this version:

template <class T>
concept Oneable = requires { []() { T n = 1; }; };

is not going to work - the failure for T=int* happens in the lambda body. That is outside of the immediate context of the expression.


This version:

template <class T>
struct S { T n = 1; };

template <class T>
concept Oneable = requires { S<T>{}; };

is hard to provide a clear answer for since, again, immediate context is not well specified. This one probably should be made to work (in a way that the lambda case never will be), and gcc for instance just makes default member initializers part of the immediate context since that's the most user-friendly option. Otherwise you end up with like:

template <typename T>
struct A {
    T var = T();
    A() = default;
};

template <typename T>
struct B {
    T var = T();
    B() requires std::default_initializable<T> = default;
};

B is obviously correct, but it's kind of a ridiculous thing to have to write, so it'd be nice if A were correct also.


Best thing to do right now if you want to be reliable across all compilers is to try to avoid ambiguous situations like this. Write code like B, not like A.

  • Related