Concepts in Concept Member Functions
I'm working pretty heavily with C++20 Concepts at the moment. Mostly to replace old runtime inheritance based interfaces with static ones.
Here is an example:
template<typename V>
concept Vector3Concept = requires(V v) {
{ v.x } -> std::same_as<float&>;
{ v.y } -> std::same_as<float&>;
{ v.z } -> std::same_as<float&>;
};
struct Vector3 {
float x,y,z;
};
static_assert(Vector3Concept<Vector3>);
This project deals with Vector3 objects. They have x,y, and z members. I have a concept to enforce the interface requirements, basic implementation, and an assertion that Vector3 meets the requirements of Vector3Concept.
template<typename E>
concept EntityConcept = requires(
E e,
Vector3Concept v) {
{ e.setPosition(v) };
};
struct Entity {
void setPosition(Vector3 v);
};
static_assert(EntityConcept<Entity>);
I then have a similar setup for an Entity. Concept, basic implementation and an assertion.
This is where we hit our problem. EntityConcept does not like having a nested concept used as a member function parameter. Depending on how you arrange things your compiler may recommend decltype/auto to be added here, or complain about associative concepts not being supported.
Entity has a setPosition member function that takes a Vector3. However our EntityConcept doesn't know about Vector3, only the Vector3Concept. This is an important idea that allows us to switch out implementations (such as using a mock version of vector3 for tests or to switch from glm to another maths library).
So, what do we do?
The solution I came across was using type_traits, specifically function_traits for member functions. This article served as my base https://functionalcpp.wordpress.com/2013/08/05/function-traits/
Then I added a small wrapper template to make things a bit clearer in the Concept
template<std::size_t ParamIndex, class MemberFunc>
struct function_param_type {
using type = typename function_traits<MemberFunc>::template argument<ParamIndex>::type;
};
OK, now this is what our EntityConcept looks like
template<typename E>
concept EntityConcept = requires(
E e,
typename function_param_type<1, decltype(&E::setPosition)>::type v) {
{ v } -> Vector3Concept;
{ e.setPosition(v) };
};
typename function_param_type<1, decltype(&E::setPosition)>::type
looks at the type of the first parameter to the setPosition function Vector3
and uses that as our type for v
.
We use 1
here for first parameter instead of 0
. Because this is a member function 0
will return Entity
instead of Vector3
.
This line correctly populates the type into our requires clause, but at the moment there's no restriction on what that type could be. To ensure that we only accept types that adhere to Vector3Concept I also added this check to the body of the concept { v } -> Vector3Concept;
.
And that's it. We can now use Concepts in our Concepts member function parameters. Best of all we're deriving the implementation of Vector3Concept from Entity itself. In this case it's a hardcoded type, but it could also be another template parameter. This is all handled without needing to specify extra template parameters such as EntityConcept<Entity, Vector3> entity
.