Search code examples
c++wrapperc++20c++-concepts

Constant version of a concept as return type in requires clause


I have some class A that has some const and some non-const function, together with a fitting concept, say

class A {
public:
    void modify() {/* ... */}
    void print() const {/* ... */}
}

template<typename T>
concept LikeA = requires (T t, const T const_t) {
    { t.modify() } -> std::same_as<void>;
    { const_t.print() } -> std::same_as<void>;
}
static_assert(LikeA<A>);

A nice thing I noticed is that for some function taking const LikeA auto &a below code is actually legal:

void use (const LikeA auto &a) {
    a.print(); // fine, print is a const method
    // a.modify(); // illegal, a is a const reference [at least for use(A a) itself, but this is not my point here]
}

static_assert(!LikeA<const A>);

int main() {
    A a1;
    use(a1); //clear, as LikeA<A> holds
    const A a2;
    use(a2); //not obvious, as LikeA<const A> does not hold
}

I looked into the definitions on cppreference, and I could not really explain this behaviour, I expected it to be illegal, although intuitively, this really is what I want.

Now on to my real situation: I have a holder class AHolder that returns const A& as one of its methods, and I want a fitting concept for this holder class that also applies to any other holder holding anything that satisfies LikeA, so I tried:

class AHolder {
public:
    const A& getA() {return _a;}
private:
    const A _a;
};

template<typename T>
concept LikeAHolder = requires (T t) {
    {t.getA() } -> LikeA;
};

static_assert(LikeAHolder<AHolder>); //fails

This fails, since const A& simply doesn't satisfy LikeA, so i would love to adjust this to

template<typename T>
concept LikeAHolder = requires (T t) {
    {t.getA() } -> const LikeA; //invalid syntax
};

static_assert(LikeAHolder<AHolder>);

in similar spirit of the example with the use method also accepting const A.

Is there such a syntax to require the return type of t.getA to satisfy LikeA whilst considering that the return type will be const?

Additionally, how exactly are concepts checked in the use(const LikeA auto &a) method such that it behaves like explained?

(My first question is the more important one for me)


Some possible solutions that I have considered:

  • Return a non-const-reference. This would make above code illegal, but of course would heavily ruin const-correctness, since _a would also have to be non-const and a user could just change the private attribute of AHolder. This is no option for me.
  • Have two concepts LikeA and LikeConstA. The return type then could be LikeConstA only requiring the const methods. This should work, but feels really clumsy and really not how concepts should be used, also this introduces more concepts that necessary to an end-user, who has to bother with them, etc.
  • In the concept LikeA, check whether the templated type T is constant (via std::is_const), and if so, don't require the non-const-methods. This works in above example, but has the undesired effect that we now simply have
class B {
public:
    void print() const {/* ... */}
}

static_assert(LikeA<const B>);

(for an already adapted LikeA, of course), which also just feels wrong.

  • in the definition of LikeA, use std::remove_reference and std::const_cast to cast away references / constness, then check for the required functions. First, i don't know if this will always work for more complicated types, but even then, this now has the undesired effect that
static_assert(LikeA<const A>);

will be true, breaking (or at least bending) the semantics of the concept.

To summary and ensure you don't get me wrong, I would like to have a way that

  • does enforce const-correctness
  • does not use a second concept 'for the end-user'. With this I mean that it is of course okay to define auxiliary concepts or use some of the standard-library that help in defining above concept, but nothing that is actually required to use A and LikeA etc.
  • does not simply ignore non-const requirements for const types (as I mentioned, this would be compiler-wise okay, but semantically feels wrong)
  • does not define LikeA<const A> to be true

Ideally, there would just be a feature working like the already-mentioned

template<typename T>
concept LikeAHolder = requires (T t) {
    {t.getA() } -> const LikeA; //invalid syntax
};

Solution

  • {[](const LikeA auto&){}( t.getA() )}
    

    Note that a non-const& returning getA will pass this.

    I made a lambda that does a concept check, then ensured t.getA() passes it.

    void use (const LikeA auto &a)
    

    This is shorthand for

    template<LikeA A>
    void use (const A&a)
    

    and when called with a T const, it deduces A=T not A=T const. Why? Because it is "more correct" abstractly. Concretely, there are a pile of rules for how template argument type deduction works.