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!
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.