Search code examples
c++templatespipelinefunctor

C++ push-pipeline with multi-functors


Is it possible to chain 3 or more multi-functors with in a pipeline functionality and an elegant syntax like pipeline?

struct Filter0{//multi-functor; will be the last one in pipeline
  template<Args...args> void operator()(Args&&...args){/*not important what is done here*/}
};

template<std::invocable F> struct Filter1{//intermediate filter forwarding to a multi-functor
  template<Args...args> void operator()(F&f, Args&&...args){
    /*do smtg and sometime call f(std::forward<decltype(args)>(args)...);*/
  }
};

template<typename Left, std::invocable Right>
auto operator|(Left left, Right right){//chains "something" with a multi-functor
    return [left,right](auto&& arg){
        left(right, std::forward<decltype(arg)>(arg));
    };
}

Filter0 filter0;
Filter1<decltype(Filter0)> filter1;
auto pipe1 = filter1 | filter0;//OK
pipe1('x');//OK pushes 'x' down the pipeline

Filter2<decltype(Filter1)> filter2;//Filter2 would be something similar with Filter1
auto pipe2 = filter2 | (filter1 | filter0);//KO
pipe2('x');//KO

With or without parentheses in 1st KO line:

  • with parens I think "(filter1 | filter0)" should somehow return a functor
  • without parens I think "filter2 | filter1" should return somehow a partial function

Note: I can "chain" 3 or more filters if I provide "the next one" as ctor's argument for each intermediate filter and store it as member inside the class instead having as argument in each method, something like:

Filter0 filter0;
Filter1 filter1(filter0);
Filter2 filter2(filter1);
filter2('x');//this will push 'x' down the pipeline

but in this case they are "tight" with each other from declaration. I would like them as "loose" as possible, "tight" only by type (or even not at all if possible)

You can play with a live example here: https://godbolt.org/z/6e8cfrbTo

SOLUTION 1 (simple & short): move template argument from filter class to methods (thanks RedFog & Jarod42!)

SOLUTION 2 (a bit more elaborated): make filter|filter operation an expression template (see https://godbolt.org/z/G9z3qfsj8)


Solution

  • the key problem is that you demands F, the type of the functor which filter should receive, for parameter, but you actually provide a lambda expression.

    auto pipe2 = filter2 | (filter1 | filter0);
    

    pipe2 is Filter2<Filter1<Filter0>>, has parameter type Filter1<Filter0>, but you provide [left,right](auto&& arg){ left(right, std::forward<decltype(arg)>(arg)); }; instead of an instance of Filter1<Filter0>.

    the most simple solution is, to let template parameter F belong to operator() instead of filter itself. see Demo.

    Edit: I remove the all std::invocable, it doesn't preform as it should be. I think the std::invocable constraint in operator| implies you want filter | value, so that filter | (filter | (... | value)...), but it's impossible, the filters you design don't have enough ability to check if it receives a value instead of another filter.

    Edit: | is left-associative, maybe a right-associative design will get half the results with twice the effort. and as @Jarod42 said, unconstraint operator| is dangerous. a better way is to make all your filters and the results of filter | filter under the constraint of a concept (maybe named isFilter?), so that you can filter | filter | ... filter and even filter | filter | ... filter | value.