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
.