Reputation: 1795
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):
error: constraints not satisfied for class template 'A' [with Downstream = B] using Upstream = A;
note: because 'B' does not satisfy 'HasBar' template
note: because 't.bar(x)' would be invalid: member access into incomplete type 'B'
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
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);
}
Upvotes: 0