Home > Software engineering >  Concept that requires a certain return type of member
Concept that requires a certain return type of member

Time:11-17

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.

  • Related