#include <type_traits>
template<typename T>
struct IsComplete final
: std::bool_constant<requires{sizeof(T);}>
{};
int main()
{
struct A;
static_assert(!IsComplete<A>::value); // ok
struct A{};
static_assert(IsComplete<A>::value); // error
}
I expected that the second static_assert
should be true as A is a complete type now.
Why does C++20's requires expression not behave as expected?
Expected expression. This error is produced whenever the compiler is expecting an expression on the line where the error occurred. In the following example, the trailing comma in the initializer indicates to the compiler that another expression will follow.
The answer lies in the grammar of the if statement, as defined by the C standard. The relevant parts of the grammar I've quoted below. Succinctly: the int b = 10 line is a declaration, not a statement, and the grammar for the if statement requires a statement after the conditional that it's testing.
Requires expressions The keyword requires is also used to begin a requires-expression, which is a prvalue expression of type bool that describes the constraints on some template arguments. Such an expression is true if the constraints are satisfied, and false otherwise:
The C++20 standard added constraints and concepts to the language. This addition introduced two new keywords into the language, concept and requires. The former is used to declare a concept, while the latter is used to introduce a requires expression or a requires clause.
A requires clause is a way to specify a constraint on a template argument or function declaration. The requires keyword must be followed by a constant expression. The idea is though, that this constant expression should be a concept or a conjunction/disjunction of concepts.
The expression is an unevaluated operand; only language correctness is checked. A requirement that starts with the keyword requires is always interpreted as a nested requirement. Thus a simple requirement cannot start with an unparenthesized requires-expression.
It's a wrong expectation. To start with, a class template has only one point of instantiation in a translation unit:
[temp.point]
7 ... A specialization for a class template has at most one point of instantiation within a translation unit. A specialization for any template may have points of instantiation in multiple translation units. If two different points of instantiation give a template specialization different meanings according to the one-definition rule, the program is ill-formed, no diagnostic required.
Templates never allowed for two points in the program to have a different interpretation of the template for the same set of arguments (an ODR nightmare in the general case). You basically start venturing into nasal-demon territory with your attempt at the trait.
And should you think using C++20 concepts is gonna change anything, you'll dive right into ill-formed; no diagnostic required territory if you conceptify the example
template<typename T>
concept IsComplete = requires{sizeof(T);};
int main()
{
struct A;
static_assert(!IsComplete<A>); // ok
struct A{};
static_assert(IsComplete<A>); // error or nuclear launch.
}
[temp.names]
8 ... A concept-id evaluates to true if the concept's normalized constraint-expression is satisfied ([temp.constr.constr]) by the specified template arguments and false otherwise.
[temp.constr.atomic]
3 ... If, at different points in the program, the satisfaction result is different for identical atomic constraints and template arguments, the program is ill-formed, no diagnostic required.
It's not anything new, concepts just add more of the same. A template's meaning for a specific set of arguments must not change if some property of the arguments is different in two different points in the program.
So while a concept (even in pre-C++20 SFINAE hackery) that checks if a type is complete may be written, to use it carelessly is to play with fire.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With