Why does C++20's requires expression not behave as expected?
Asked Answered
V

1

12
#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?

Vague answered 23/5, 2021 at 9:46 Comment(0)
D
20

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.

Dakar answered 23/5, 2021 at 9:56 Comment(5)
This can even be seen in action: cppinsights.io/s/67df151f Only 1 new struct is created for A and is used in both cases.Untangle
You can even "fix" it by adding a dummy, unique parameter to the template, e.g. template<typename T, int _line> and IsComplete<A, __LINE__>Wanhsien
If the two declarations of struct A are declared in different blocks we get two different types and both assertions pass: cppinsights.io/s/f7e69e33Heda
In this case, @md2perpe, that's because each struct A defines a distinct type, and thus IsComplete will be instantiated twice.Formalize
@JustinTime-ReinstateMonica. I know. That's what I wanted to show.Heda

© 2022 - 2024 — McMap. All rights reserved.