Search code examples
c++operator-overloadingc++17chainingpipelining

How to chain and serialize functions by overloading the | operator


I'm trying to figure out how to generically overload the operator|() for a given base class object to serialize or chain function calls that are similar to how pipes or operator<<() works... I'd like to chain them through the pipe operator... This way I can have a series of standalone functions, and call them on a single data object... In other words, to perform multiple transformations on the same data type, like in a streaming system...

Consider the following pseudo code sample: this code probably won't compile, I don't have my compiler handy and I may be using the wrong syntax for the function pointers or function objects as a parameter in the operators... This is only to illustrate the pattern and behavior that I'm after.

template<typename T>
typedef T(*Func)(T); // Function Pointer for functors-lambdas-etc... 

template<typename T>
struct pipe_object {
    T operator|(T(*Func)(T) func) {
        return func(T);
    }

    T operator()(T(*Func)(T) func) {
        return this->operator|(t, func);
    }
};

Then I might want to use them something like this:

constexpr int add_one_f(int x) {
    return (x+1);
}

constexpr int add_two_f(int x) {
   return (x+2);
}


void foo() {
    pipe_object<int> p1 = {};
    pipe_object<int> p2 = {};

    int result = p1(&add_one) | p2(&add_two); 

    // or something like...

    int result = p1 | p2; // ... etc ...

    // or something like:
    p1 = add_one | add_two | p2; // ... etc ...
}

I just don't know how to propagate the intput - output in the |() operator... Would I have to overload two versions so that it can recognize |(lhs, rhs) as well as |(rhs, lhs)?

More than just that, what if I want to expand this so that my functors or lambdas were to take multiple arguments...

I've been doing Google searches on this and only found a couple of resources but nothing that is concrete, simple, elegant, and up to date at least with C++17 features...

If you know of any good source materials on this subject please let me know!


Solution

  • First I assume you have some basics that look like this

    #include <iostream>
    struct vec2 {
        double x;
        double y;
    };
    std::ostream& operator<<(std::ostream& stream, vec2 v2) {return stream<<v2.x<<','<<v2.y;}
    
    //real methods
    vec2 translate(vec2 in, double a) {return vec2{in.x+a, in.y+a};} //dummy placeholder implementations
    vec2 rotate(vec2 in, double a) {return vec2{in.x+1, in.y-1};}
    vec2 scale(vec2 in, double a) {return vec2{in.x*a, in.y*a};}
    

    So what you want is a proxy class for operations, where a proxy object is constructed with the function and the "other parameters". (I made the function a template parameter, which prevents the use of function pointers, and helps the optimizer to inline, making this nearly zero overhead.)

    #include <type_traits>
    //operation proxy class
    template<class rhst, //type of the only parameter
         vec2(*f)(vec2,rhst)> //the function to call
    class vec2_op1 {
        std::decay_t<rhst> rhs; //store the parameter until the call
    public:
        vec2_op1(rhst rhs_) : rhs(std::forward<rhst>(rhs_)) {}
        vec2 operator()(vec2 lhs) {return f(lhs, std::forward<rhst>(rhs));}
    };
    
    //proxy methods
    vec2_op1<double,translate> translate(double a) {return {a};}
    vec2_op1<double,rotate> rotate(double a) {return {a};}
    vec2_op1<double,scale> scale(double a) {return {a};}
    

    And then you simply make that chainable

    //lhs is a vec2, rhs is a vec2_operation to use
    template<class rhst, vec2(*f)(vec2,rhst)>
    vec2& operator|(vec2& lhs, vec2_op1<rhst, f>&& op) {return lhs=op(lhs);}
    

    Usage is simple:

    int main() {
        vec2 v2{3,5};
        v2 | translate(2.5) | rotate(30) | translate(3) | scale(2);
        std::cout << v2;
    }
    

    http://coliru.stacked-crooked.com/a/9b58992b36ff12d3

    Note: No allocations, no pointers, no copies or moves. This should generate the same code as if you just did v2.translate(2.5); v2.rotate(30); v2.scale(10); directly.