Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Concept that requires a certain return type of member

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.

like image 526
E_3 Avatar asked Jan 25 '26 14:01

E_3


1 Answers

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 tests 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& but 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.

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 intend 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.


And as another edge case you need to consider whether you really want to accept any member or only non-static ones. All of the solutions above assume that you will accept a static member as well.


Also, you need to consider whether a member inherited from a base class should be accepted. All of the suggested solutions above do accept them.


All of the above solutions also assume that the member should be accepted only if it is public. The check in the concept is done from a context not related to any class, so accessibility based on context doesn't work anyway, and there probably is no reason to accept a private member that then wouldn't actually be usable in the function that is constrained by the concept.

like image 169
user17732522 Avatar answered Jan 27 '26 03:01

user17732522



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!