[EDIT] This works as expected on g++ 10.3, 11.2 and current clang. Some comments indicate that this is undefined behavior, so beware of possibly unexpected changes for future compilers.
Original solution:
I also just stumbled on this, and succeeded in making my CRTP with concepts work by allowing type to be either incomplete, or complete with the desired constraints.
Besides the defined IsFoo, I also define IsComplete helper (using sizeof trick) and, finally, IsFooIncomplete in the following way:
template<class T>
concept IsFooIncomplete = !IsComplete<T> || IsFoo<T>;
This way I can ensure that, during the processing of Foo, it is incomplete, and just after class is finished, it is complete and matches desired IsFoo constraints.
#include <concepts>
#include <type_traits>
template<class T>
concept IsFoo = requires(T self)
{
{
self.a
} -> std::same_as<int&>;
};
template<class T>
concept IsComplete = requires(T self)
{
{
// You can't apply sizeof to an incomplete type
sizeof(self)
};
};
template<class T>
concept IsFooIncomplete = !IsComplete<T> || IsFoo<T>;
#if 0
// Will not compile, because Foo currently has incomplete type
template<IsFoo AFoo>
struct AcceptsFoo
{};
#else
// will compile with IsFooIncomplete
template<IsFooIncomplete AFoo> // no need to use 'class AFoo' here...
struct AcceptsFoo
{};
#endif
struct Foo
{
int a;
int b;
// Foo is incomplete here, but that's fine!
static_assert(!IsComplete<Foo>);
AcceptsFoo<Foo> obj;
};
// Foo is now complete, and that's also fine!
static_assert(IsFoo<Foo>);
Works fine on g++ version 10.3.0 (with flag --std=c++20), and I hope it works on other compilers as well.
[EDIT] as pointed out in comments, this accepts any incomplete type, but thats intended. Only the external static assert will filter complete type cases. Thanks @David Herring for the example on Bar, I wrote it here for testing: https://godbolt.org/z/sqc75qqMv
[EDIT2] now this deals with the CRTP variant, without any undefined behavior and without the IsComplete workaround, just by storing the type for checking once class is complete.
// Will not compile, because Foo currently has incomplete type
template<IsFoo AFoo>
struct AcceptsFoo
{};
#else
template<class AFoo> // will not check IsFoo directly here...
struct AcceptsFoo
{
using IsFooType = AFoo; // will store type on IsFootType for later checks
};
#endif
struct Foo : public AcceptsFoo<Foo> // will check only when complete
{
int a;
int b;
};
// Foo is now complete, and that's also fine!
static_assert(IsFoo<Foo::IsFooType>);
struct FooBar : public AcceptsFoo<FooBar> // will fail once it is complete
{
//int a;
int b;
};
// will fail here
static_assert(IsFoo<FooBar::IsFooType>);
This also works on major compilers: https://godbolt.org/z/e1Gc4Kj5n