Getting started¶
Higher-order functions¶
A core part of this library is higher-order functions. A higher-order function is a function that either takes a function as its argument or returns a function. To be able to define higher-order functions, we must be able to refer functions as first-class objects. One example of a higher-order function is std::accumulate
. It takes a custom binary operator as a parameter.
One way to refer to a function is to use a function pointer(or a member function pointer). So if we had our own custom sum
function, we could pass it directly to std::accumulate
:
int sum(int x, int y)
{
return x + y;
}
// Pass sum to accumulate
std::vector<int> v = { 1, 2, 3 };
int total = std::accumulate(v.begin(), v.end(), 0, &sum);
However, a function pointer can only refer to one function in an overload set of functions, and it requires explicit casting to select that overload.
For example, if we had a templated sum
function that we want to pass to std::accumulate
, we would need an explicit cast:
template<class T, class U>
auto sum(T x, U y)
{
return x + y;
}
auto sum_int = (int (*)(int, int))∑
// Call integer overload
int i = sum_int(1, 2);
// Or pass to an algorithm
std::vector<int> v = { 1, 2, 3 };
int total = std::accumulate(v.begin(), v.end(), 0, sum_int);
Function Objects¶
A function object allows the ability to encapsulate an entire overload set into one object. This can be done by defining a class that overrides the call operator like this:
// A sum function object
struct sum_f
{
template<class T, class U>
auto operator()(T x, U y) const
{
return x + y;
}
};
There are few things to note about this. First, the call operator member function is always declared const
, which is generally required to be used with Boost.HigherOrderFunctions.(Note: The mutable adaptor can be used to make a mutable function object have a const
call operator, but this should generally be avoided). Secondly, the sum_f
class must be constructed first before it can be called:
auto sum = sum_f();
// Call sum function
auto three = sum(1, 2);
// Or pass to an algorithm
std::vector<int> v = { 1, 2, 3 };
int total = std::accumulate(v.begin(), v.end(), 0, sum);
Because the function is templated, it can be called on any type that has the plus +
operator, not just integers. Futhermore, the sum
variable can be used to refer to the entire overload set.
Lifting functions¶
Another alternative to defining a function object, is to lift the templated function using BOOST_HOF_LIFT. This will turn the entire overload set into one object like a function object:
template<class T, class U>
auto sum(T x, U y)
{
return x + y;
}
// Pass sum to an algorithm
std::vector<int> v = { 1, 2, 3 };
int total = std::accumulate(v.begin(), v.end(), 0, BOOST_HOF_LIFT(sum));
However, due to limitations in C++14 this will not preserve constexpr
. In those cases, its better to use a function object.
Declaring functions¶
Now, this is useful for local functions. However, many times we want to write functions and make them available for others to use. Boost.HigherOrderFunctions provides BOOST_HOF_STATIC_FUNCTION to declare the function object at the global or namespace scope:
BOOST_HOF_STATIC_FUNCTION(sum) = sum_f();
The BOOST_HOF_STATIC_FUNCTION declares a global variable following the best practices as outlined in N4381. This includes using const
to avoid global state, compile-time initialization of the function object to avoid the static initialization order fiasco, and an external address of the function object that is the same across translation units to avoid possible One-Definition-Rule(ODR) violations. In C++17, this can be achieved using an inline
variable:
inline const constexpr auto sum = sum_f{};
The BOOST_HOF_STATIC_FUNCTION macro provides a portable way to do this that supports pre-C++17 compilers and MSVC.
Adaptors¶
Now we have defined the function as a function object, we can add new “enhancements” to the function. One enhancement is to write “extension” methods. The proposal N4165 for Unified Call Syntax(UFCS) would have allowed a function call of x.f(y)
to become f(x, y)
. Without UFCS in C++, we can instead use pipable function which would transform x | f(y)
into f(x, y)
. To make sum_f
function pipable using the pipable adaptor, we can simply write:
BOOST_HOF_STATIC_FUNCTION(sum) = pipable(sum_f());
Then the parameters can be piped into it, like this:
auto three = 1 | sum(2);
Pipable function can be chained mutliple times just like the .
operator:
auto four = 1 | sum(2) | sum(1);
Alternatively, instead of using the |
operator, pipable functions can be chained together using the flow adaptor:
auto four = flow(sum(2), sum(1))(1);
Another enhancement that can be done to functions is defining named infix operators using the infix adaptor:
BOOST_HOF_STATIC_FUNCTION(sum) = infix(sum_f());
And it could be called like this:
auto three = 1 <sum> 2;
In addition, adaptors are provided that support simple functional operations such as partial application and function composition:
auto add_1 = partial(sum)(1);
auto add_2 = compose(add_1, add_1);
auto three = add_2(1);
Lambdas¶
Writing function objects can be a little verbose. C++ provides lambdas which have a much terser syntax for defining functions. Of course, lambdas can work with all the adaptors in the library, however, if we want to declare a function using lambdas, BOOST_HOF_STATIC_FUNCTION won’t work. Instead, BOOST_HOF_STATIC_LAMBDA_FUNCTION can be used to the declare the lambda as a function instead, this will initialize the function at compile-time and avoid possible ODR violations:
BOOST_HOF_STATIC_LAMBDA_FUNCTION(sum) = [](auto x, auto y)
{
return x + y;
};
Additionally, adaptors can be used, so the pipable version of sum
can be written like this:
// Pipable sum
BOOST_HOF_STATIC_LAMBDA_FUNCTION(sum) = pipable([](auto x, auto y)
{
return x + y;
});