Search code examples
c++c++20c++-concepts

Designing a C++ concept with multiple invocables / predicates


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?


Solution

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