Reputation: 6200
I stumbled upon this:
#include <type_traits>
#include <concepts>
template<class T>
concept IsFoo = requires(T a)
{
{a.a} -> std::same_as<int>;
};
#if 1
// Will not compile, because Foo currently has incomplete type
template<IsFoo AFoo>
struct AcceptsFoo
{};
#else
template<class AFoo>
struct AcceptsFoo
{};
#endif
struct Foo
{
int a;
int b;
AcceptsFoo<Foo> obj;
};
https://gcc.godbolt.org/z/j43s4z
Other variant (crtp) https://gcc.godbolt.org/z/GoWfhq
Foo is incomplete because it has to instantiate AcceptsFoo
, but to do so, Foo
has to be complete, otherwise it cannot check IsFoo
. Is this a bug in GCC, or does the standard say so? The latter would be sad, because this prevents concepts for being used together with some well-known patterns such as CRTP.
I noticed that clang does gives a similar error: https://gcc.godbolt.org/z/d5bEez
Upvotes: 6
Views: 1386
Reputation: 99
[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
Upvotes: -1
Reputation: 302932
You can check a concept against an incomplete type - but if that concept check requires actually doing anything with the type that requires it to complete, that's going to fail the concept check.
And even there, you have to be careful since implementations are allowed to (and will) cache concept checks to compile faster - so if you try C<T>
while T
is incomplete and try again when T
becomes complete, and those should give different answers, you're asking for trouble.
Foo
isn't complete at the point you're checking it, so naturally it doesn't have a member named a
. This really cannot possibly work.
because this prevents concepts for being used together with some well-known patterns such as CRTP.
Being used together in this way, yes. In the same way that with CRTP you also can't access anything directly off of the template parameter passed into the base class, you always have to be careful to delay any instantiation of that type until it is complete.
This is ultimately the same reason:
template <typename Derived>
struct B {
typename Derived::type x;
};
struct D : B<D> {
using type = int;
};
does not work.
Upvotes: 8