Pipable functions in C++14
Pipable functions allow us to write extension methods in C++. This overloads the pipe |
operator to allow chaining several functions together, like the example below:
int number_3 = 1 | add_one | add_one;
std::cout << number_3 << std::endl;
Let’s look how to implement this in C++14. First, lets start by implementing a pipe_closure
class to handle a function with just one parameter:
template<class F>
struct pipe_closure : F
{
template<class... Xs>
pipe_closure(Xs&&... xs) : F(std::forward<Xs>(xs)...)
{}
};
template<class T, class F>
decltype(auto) operator|(T&& x, const pipe_closure<F>& p)
{
return p(std::forward<T>(x));
}
So this overloads the |
pipe operator and passes the parameter on to the function. So now defining a add_one
function object:
struct add_one_f
{
template<class T>
auto operator()(T x) const
{
return x + 1;
}
};
We can initialize it and use it like this:
const pipe_closure<add_one_f> add_one = {};
int number_3 = 1 | add_one | add_one;
std::cout << number_3 << std::endl;
Now, this only supports one parameter, and we can’t statically initialize the function object using constexpr
because of the constructor. Lets look at how we can build on top of this to fix those things so that we could, say for instance, call a sum
function like this:
int number_3 = 1 | sum(2);
std::cout << number_3 << std::endl;
Lets create a pipable
class that returns the pipe_closure
class with a lambda that has just one parameter, but will call the function with the rest of the parameters:
template<class F>
auto make_pipe_closure(F f)
{
return pipe_closure<F>(std::move(f));
}
template<class F>
struct pipable
{
template<class... Ts>
auto operator()(Ts... xs) const
{
return make_pipe_closure([=](auto x) -> decltype(auto)
{
return F()(x, xs...);
});
}
};
Also, because we default construct the function object, the pipable
class can now be initialized using constexpr
. So now we can use it like this:
const constexpr pipable<sum_f> sum = {};
int number_3 = 1 | sum(2);
std::cout << number_3 << std::endl;
Notice, however, this actually makes a copy of all its arguments by value. Ideally, it would be better to capture the parameters “perfectly”. However, C++ lambdas don’t provide a perfect capture, so lets create a wrapper to store references and values:
#define REQUIRES(...) class=std::enable_if_t<(__VA_ARGS__)>
template<class T>
struct wrapper
{
T value;
template<class X, REQUIRES(std::is_convertible<T, X>())>
wrapper(X&& x) : value(std::forward<X>(x))
{}
T get() const
{
return std::move(value);
}
};
template<class T>
auto make_wrapper(T&& x)
{
return wrapper<T>(std::forward<T>(x));
}
We constraint the constructor above(using REQUIRES
), because universal references can overload the copy/move constructors. Now, unfortunately, we can’t capture the wrappers using C++’s init-captures, since they don’t work with varidiac packs. So, instead, we pass it in as a parameter:
template<class F>
struct pipable
{
template<class... Ts>
auto operator()(Ts&&... xs) const
{
return make_pipe_closure([](auto... ws)
{
return [=](auto&& x) -> decltype(auto)
{
return F()(x, ws.get()...);
};
}(make_wrapper(std::forward<Ts>(xs)...)));
}
};
Additionally, we can add another overload for the |
pipe operator with pipable
, so it will work for unary functions without the call operator(such as add_one
shown at the begining of the post):
template<class T, class F>
decltype(auto) operator|(T&& x, const pipable<F>& p)
{
return F()(std::forward<T>(x));
}
So now we can initialize and use add_one
, like this:
const constexpr pipable<add_one_f> add_one = {};
int number_3 = 1 | add_one | add_one;
std::cout << number_3 << std::endl;