I have some trouble getting started with C 20 concepts. I want to define a concept that requires a class to have a member called count_
that must be of type int
:
#include <concepts>
template <typename T>
concept HasCount = requires(T thing) {
{ thing.count_ } -> std::same_as<int>;
};
The following struct should satisfy this concept:
struct BaseTableChunk {
BaseTableChunk* next_;
int count_ = 0;
int data_[1000];
};
Then, the following code does not compile:
template <HasCount Chunk>
class BaseTable {
void doSomething();
};
int main() {
BaseTable<BaseTableChunk> table{};
return 0;
}
The compiler gives the following error:
note: constraints not satisfied
In file included from /usr/include/c /10/compare:39,
from /usr/include/c /10/bits/stl_pair.h:65,
from /usr/include/c /10/bits/stl_algobase.h:64,
from /usr/include/c /10/bits/char_traits.h:39,
from /usr/include/c /10/ios:40,
from /usr/include/c /10/ostream:38,
from /usr/include/c /10/iostream:39,
from Minimal2.cxx:1:
/usr/include/c /10/concepts:57:15: required for the satisfaction of ‘__same_as<_Tp, _Up>’ [with _Tp = int&; _Up = int]
/usr/include/c /10/concepts:62:13: required for the satisfaction of ‘same_as<int&, int>’
/usr/include/c /10/concepts:57:32: note: the expression ‘is_same_v<_Tp, _Up> [with _Tp = int&; _Up = int]’ evaluated to ‘false’
57 | concept __same_as = std::is_same_v<_Tp, _Up>;
As I understand it, thing.count_
is evaluated to return an int&
instead of an int
, which is not what I'd expect.
Should I instead test for { thing.count_ } -> std::same_as<int&>
? (Which then does compile.) That seems rather counter-intuitive to me.
CodePudding user response:
If count_
is a member of thing
with declared type int
, then the expression thing.count_
is also of type int
and the expression's value category is lvalue.
A compound requirement of the form { E } -> C
will test whether decltype((E))
satisfies C
. In other words it test whether the type of the expression E
, not the type of the entity that E
might name, satisfies the concept.
The type of the expression as obtained by decltype((E))
translates the value category to reference-qualification of the type. Prvalues result in non-references, lvalues in lvalue references and xvalues in rvalue references.
So in your example the type will be int&
and the concept std::same_as
requires strict match of the type, including its reference-qualification, making it fail.
A simple solution would be to just test against int&
:
{ thing.count_ } -> std::same_as<int&>;
A similar solution as mentioned also in the question comments is since C 23:
{ auto(thing.count_) } -> std::same_as<int>
auto
is deduced to int
and the functional-style cast expression int(...)
is always a prvalue, so that it will never result in a reference-qualified type.
Or another alternative is to write a concept to replace std::same_as
that doesn't check exact type equality but applies std::remove_reference_t
or std::remove_cvref_t
to the type first, depending on how you want to handle const
-mismatch. Notably the first solution will not accept a const int
member or const
-qualified T
with int
member, while the second one will (because auto
never deduces a const
).
However, you should be careful here if you intent to check that thing
has a member of type int
exactly. All of the solutions above will also be satisfied if it has a thing
member of type reference-to-int
.
Excluding the reference case cannot be done with a compound requirement easily, but a nested requirement may be used instead:
template <typename T>
concept HasCount = requires(T thing) {
requires std::same_as<decltype(thing.count_), int>;
};
The nested requirement (introduced by another requires
) checks not only validity of the expression but also whether it evaluates to true
. The difference in the check here is that I use decltype(thing.count_)
instead of decltype((thing.count_))
. decltype
has an exception when it names a member directly through a member access expression (unparenthesized). In that case it will produce the type of the named entity, not of the expression. This verifies that count_
is a int
, not a int&
.
There are also further edge cases you should consider if T
is const
-qualified or a reference type. You should carefully consider under which conditions exactly the concept should be satisfied in these cases. Depending on the answer the suggested solutions may or may not be sufficient.