user5406764
user5406764

Reputation: 1795

In C++ how can I use concepts with composition (not CRTP-style inheritance) to enforce an interface?

I'm trying to use C++20 concept to enforce an interface. I'm using a templated class 'A' which calls into class 'B'. The trick is, instead of using CRTP (inheritance) I'm using ownership (composition). It works WITHOUT CONCEPTS, but does not work WITH CONCEPTS.

Here's some code to illustrate:

#include <concepts>

// =========================================================================

// This is the interface I'm trying to enforce
template<typename T>
concept HasBar = requires(T t, int x)
{
    { t.bar(x) } -> std::same_as<int>;
};

template<HasBar Downstream> // If you change "HasBar" to "typename" it compiles
struct A
{
    A(Downstream& downstream) :
        downstream_{downstream}
    {}

    int foo(int x) { return downstream_.bar(x); }

private:
    Downstream& downstream_;
};

// =========================================================================

// Not using CRTP
struct B
{
    using Upstream = A<B>;
    int bar(int x) { return x + 1; }

    Upstream upstream_{*this}; // Instead we own the parent
};

// =========================================================================

int main()
{
    B b;

    return b.upstream_.foo(1);
}

(Godbolt: https://godbolt.org/z/xE3G5nM5n)

The error messages (which I've tried to clean up so only the relevant info is shown):

So can we not use concepts with templated composition patterns like this because of how a concept needs the full type information at the point of use?

NOTE: I did see this: Can concepts be used with CRTP idiom?

Which means that I could change the code to the following, less pretty form:

#include <concepts>

// =========================================================================

// This is the interface I'm trying to enforce
template<typename T>
concept HasBar = requires(T t, int x)
{
    { t.bar(x) } -> std::same_as<int>;
};

template<typename Downstream> // Back to "typename"
struct A
{
    A(Downstream& downstream) :
        downstream_{downstream}
    {
        static_assert(HasBar<Downstream>); // Aha!
    }

    int foo(int x) { return downstream_.bar(x); }

private:
    Downstream& downstream_;
};

// =========================================================================

// Not using CRTP
struct B
{
    using Upstream = A<B>;
    int bar(int x) { return x + 1; }

    Upstream upstream_{*this}; // Instead we own the parent
};

// =========================================================================

int main()
{
    B b;

    return b.upstream_.foo(1);
}

Perhaps I've just answered my own question then, but is this the "right" way to do it? Are there any downsides to using static_assert() instead of specifying the constraint in the template?

Thanks!

Upvotes: 0

Views: 113

Answers (1)

463035818_is_not_an_ai
463035818_is_not_an_ai

Reputation: 123114

Using the static assert merely prevents to call the constructor, not more. For example A<int> would still be an "ok" type. That's usually not desirable.

You cannot constrain template arguments based on their members when they are incomplete. Though, you just have to go one step further with the composition:

struct B {
    int bar(int x) { return x + 1; }    
};

struct C {
    B b;
    A<B> a{b};   
};

// =========================================================================

int main()
{
    C c;

    return c.a.foo(1);
}

Full example

Upvotes: 0

Related Questions