I'm trying to make a C++20 concept by using an existing class as a blueprint. The existing class has 8 member functions that each take in a predicate:
struct MyGraphClass {
auto get_outputs(auto n, auto predicate) { /* body removed */ };
auto get_output(auto n, auto predicate) { /* body removed */ }
auto get_outputs_full(auto n, auto predicate) { /* body removed */ }
auto get_output_full(auto n, auto predicate) { /* body removed */ }
auto get_inputs(auto n, auto predicate) { /* body removed */ }
auto get_input(auto n, auto predicate) { /* body removed */ }
auto get_inputs_full(auto n, auto predicate) { /* body removed */}
auto get_input_full(auto n, auto predicate) { /* body removed */ }
/* remainder of class removed */
}
To support this in my concept, I've used 8 different concept parameters of std::predicate
, one for each member function. The reason I did this (as opposed to using a single concept parameter for all predicates) is that the type of each predicate isn't known before-hand (e.g. an invocation of get_outputs(n, predicate)
could be using a function pointer for the predicate while an invocation of get_inputs(n, predicate)
could be using a functor for the predicate).
template<typename P, typename E, typename N, typename ED>
concept EdgePredicateConcept = std::predicate<P, E, N, N, ED>;
template<typename DG, typename N, typename ND, typename E, typename ED, typename EP1, typename EP2, typename EP3, typename EP4, tpyename EP5, typename EP6, typename EP7, typename EP8>
concept ConstantDirectedGraphConcept =
requires EdgePredicateConcept<EP1, E, N, ED>
&& requires EdgePredicateConcept<EP2, E, N, ED>
&& requires EdgePredicateConcept<EP3, E, N, ED>
&& requires EdgePredicateConcept<EP4, E, N, ED>
&& requires EdgePredicateConcept<EP5, E, N, ED>
&& requires EdgePredicateConcept<EP6, E, N, ED>
&& requires EdgePredicateConcept<EP7, E, N, ED>
&& requires EdgePredicateConcept<EP8, E, N, ED>
&& requires(DG g, N n, ND nd, E e, ED ed, EP1 ep1, EP2 ep2, EP3 ep3, EP4 ep4, EP5 ep5, EP6 ep6, EP7 ep7, EP8 ep8) {
{ g.get_outputs(n, ep1) } -> /* return removed */;
{ g.get_output(n, ep2) } -> /* return removed */;
{ g.get_outputs_full(n, ep3) } -> /* return removed */;
{ g.get_output_full(n, ep4) } -> /* return removed */;
{ g.get_inputs(n, ep5) } -> /* return removed */;
{ g.get_input(n, ep6) } -> /* return removed */;
{ g.get_inputs_full(n, ep7) } -> /* return removed */;
{ g.get_input_full(n, ep8) } -> /* return removed */;
/* remainder of requirements removed */
};
Since I don't know the exact types of the predicates beforehand, I'm not sure what to set EP1
- EP8
to when I'm applying this concept. Even if I did, the number of concept parameters makes this concept difficult to use.
Is there a sane way of getting this to work?
I would do it this way.
First, your graph concept has several associated types. Similar to how a range
in C++ has an iterator
type (amongst others). We don't write range<R, I>
, we just write range<R>
. If R
is a range, then it has some iterator type - we don't ask if R
is a range with iterator I
. We may not know I
ex ante. Similarly, we have input_iterator<I>
, and not input_iterator<I, value_type, reference, difference_type>
. I
defines those other things (if it's actually an iterator
).
So we'll start with some aliases:
template <class G> using node_type = /* ... */;
template <class G> using edge_type = /* ... */;
// ...
Next, you want your graph type to accept an arbitrary predicate. There's no way to phrase arbitrary predicate in C++. But we can do the next best thing: just pick one arbitrary one. If the graph type works for some arbitrary private type that you define, it probably works for any such thing. Thus:
namespace impl {
template <class G>
struct some_predicate {
// intentionally not default constructible
// since predicate doesn't have to be
some_predicate() = delete;
// likewise doesn't have to be copyable, though you
// may just want to default these anyway (to allow
// having by-value predicates, for instance)
some_predicate(some_predicate const&) = delete;
some_predicate& operator=(some_predicate const&) = delete;
// but it does need this call operator
auto operator()(edge_type<G> const&,
node_type<G> const&,
node_type<G> const&,
edge_data_type<G> const&) const
-> bool;
};
}
We don't need to define that call operator since we're not using it anyway. But we can use some_predicate
to build up our concept:
template <typename G>
concept ConstantDirectedGraphConcept =
requires(G g, node_type<G> n, impl::some_predicate<G> p) {
g.get_outputs(n, p);
g.get_output(n, p);
g.get_outputs_full(n, p);
g.get_output_full(n, p);
g.get_inputs(n, p);
g.get_input(n, p);
g.get_inputs_full(n, p);
g.get_input_full(n, p);
};
That should get you most of the way there. If a user's graph type works with this arbitrary predicate, then it probably will work for any arbitrary predicate.
Possibly (depending on how node_type
and friends are defined) this needs to be:
template <typename G>
concept ConstantDirectedGraphConcept = requires {
typename node_type<G>;
typename edge_type<G>;
typename node_data_type<G>;
typename edge_data_type<G>;
} && requires(G g, node_type<G> n, impl::some_predicate<G> p) {
// ...
}
};