This code compiles:
#include <iostream>
class MyClass;
template<typename T>
concept HasFun = requires(T t)
{
{ t.fun() };
};
int main()
{
std::cout << HasFun<MyClass> << std::endl;
return 0;
}
without an error and prints 0
.
When
class MyClass;
is replaced with
class MyClass
{
public:
void fun();
};
it prints 1
;
My expectation is for compilation to fail (in the first case) with an error saying something like "aggregate ‘MyClass’ has incomplete type".
What use case is there in allowing concepts to evaluate class declarations?
class MyClass; template<typename T> concept HasFun = requires(T t) { { t.fun() }; }; // ... HasFun<MyClass>
My expectation is for compilation to fail (in the first case) with an error saying something like "aggregate ‘MyClass’ has incomplete type".
No, that wouldn't happen.
A requires-expression checks whether t.fun()
is a valid expression,
and it isn't valid because t
has incomplete type.
However, this simply means that the expression is false
, not that the program is ill-formed.
Even though it "works", using concepts with incomplete types is quite dangerous. See Can a concept be checked against incomplete type. To sum up the issues:
requires
in a concept) don't produce the same result everywhere, the program is ill-formed, no diagnostic required ([temp.constr.atomic]).std::is_trivially_copyable
lead to UB when used with an incomplete type ([meta.unary.prop]).In your case, the program would be ill-formed, no diagnostic required, if you first checked HasFun<MyClass>
with incomplete MyClass
,
and checked it again later with complete MyClass
.
In practice, compilers can cache the result of HasFun<MyClass>
so they perform only one check, and you get bogus results if this cached result is incorrect.
This is certainly a huge footgun, but some concepts can be used with incomplete types just fine:
// Whether a type is complete or not,
// we can consistently check this concept for satisfaction.
template <typename T>
concept object_type = !std::is_reference_v<T>
&& !std::is_void_v<T>
&& !std::is_function_v<T>;
// Useful: containers of incomplete types.
template <object_type T>
class container { /* ... */ };
It's perfectly reasonable to have containers of incomplete types.
For example, std::vector<T>
doesn't require a complete T
(since C++11),
but operations like push_back
require a complete type.