Home > Software design >  Designing a C concept with multiple invocables / predicates
Designing a C concept with multiple invocables / predicates

Time:05-10

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?

CodePudding user response:

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) {
        // ...
    }
};
  • Related