Jonathan Boccara's blog

4 Features of Boost HOF That Will Make Your Code Simpler

Published January 15, 2021 - 0 Comments

Daily C++

Boost HOF, standing for Higher Order Functions, is a Boost library offering functions that work on functions.

This impressive library provides a lot of advanced components allowing to go a step further into functional programming in C++. In this post, we’ll focus on 4 of the more basic ones (+ a bonus one) that allow to make code simpler in common tasks.

HOF provides one header in the form of #include <boost/hof/XXX.hpp> for each component, as well as a general header #include <boost/hof.hpp>. It is compatible with C++11.

first_of: simplified overloading

When designing generic functions, various sorts of types can require various implementations.

Consider for example the case of a generic function that converts data to a std::string. Let’s call that function my_to_string.  The implementation of my_to_string depends on the input type.

If the input type is a std::string, then there is nothing to do. If the input type is a primitive type, we can use the standard function std::to_string. And to convert collections such as std::vector, std::map or any type of range, we need to iterate on the range and print each component.

It would be nice to implement my_to_string with code like this:

std::string const& my_to_string(std::string const& s)
{
    return s;
}

template<typename T>
std::string my_to_string(T const& value)
{
    return std::to_string(value);
}

template<typename Range>
std::string my_to_string(Range const& range)
{
    std::ostringstream result;
    for (auto const& value : range)
    {
        result << value << ' ';
    }
    return result.str();
}

However, this code doesn’t work. For example, calling my_to_string(42) is ambiguous: there are two overloads that could fit: the second one and the third one.

To disambiguate, we would need to specialize the templates with SFINAE, but then we’d enter the tricky topic of partial specialisation and overloading. Or we could think of something with C++20 concepts.

However, when we look at the implementation of the overloads, we see that only one would make sense. Indeed, the second one works well on input 42.

How nice would it be to tell the compiler “try each overload and takes the first one that works”?

This is exactly what boost::hof::first_of is made for.

With first_of, we can rewrite our code like this:

auto my_to_string = boost::hof::first_of(
    [](std::string const& s) -> std::string const&
    {
        return s;
    },
    [](auto const& value) -> decltype(std::to_string(value))
    {
        return std::to_string(value);
    },
    [](auto const& range)
    {
        std::ostringstream result;
        for (auto const& value : range)
        {
            result << value << ' ';
        }
        return result.str();
    }
);

For a given input, first_of considers the various functions we pass it, and invokes the first one that works.

Note though that it doesn’t make its choice based on the implementation of each function, but rather on its prototype. This is why we make std::to_string appear in the prototype of the second one.

first_of encapsulates all the SFINAE machinery that tries each overload in order, and lets us provide the various candidates in order.

construct: a function object representing a constructor

C++ allows to pass free functions or member function as arguments to other functions. But there is one type of function that C++ doesn’t allow to pass along: class constructors.

Consider the example where we want to transform a collection of objects of a type into a collection of objects of another type constructed from the first one.

Let’s see an example. The Circle class can be constructed from an double:

class Circle
{
public:
    explicit Circle(double radius) : radius_(radius) {}
    
    double radius() const { return radius_; };

    // rest of the Circle’s interface
    
private:
    double radius_;    
};

To transform a collection of doubles into a collection of Circless, passing the constructor doesn’t compile, as we are not allowed to take the address of a constructor:

auto const input = std::vector<double>{1, 2, 3, 4, 5};
auto results = std::vector<Circle>{};

std::transform(begin(input), end(input), back_inserter(results), &Circle::Circle); // imaginary C++

How do we do then?

We can use boost::hof::construct:

auto const input = std::vector<double>{1, 2, 3, 4, 5};
auto results = std::vector<Circle>{};

std::transform(begin(input), end(input), back_inserter(results), boost::hof::construct<Circle>());

proj: projecting on a function

HOF’s proj allows a function to work on a transformation of its input as opposed to its input itself.

To illustrate, let’s consider a case where we want to sort objects of the above Circle class:

auto circles = std::vector<Circle>{ Circle{2}, Circle{1}, Circle{3}, Circle{0.5} }; // not in sorted order

Let’s assume that Circle doesn’t provide any comparison operator, but for the purpose of the sort we’d like to sort circles in ascending order of their radii.

With a (pre-C++20) STL algorithm, we’d write:

std::sort(begin(circles), end(circles), [](Circle const& circle1, Circle const& circle2)
                                        {
                                            return circle1.radius() < circle2.radius();
                                        });

But it would be nicer to simply tell std::sort that we’d like to use radius() and not the whole Circle, instead of writing all this code.

C++20 ranges algorithms allow to do that with projectors:

std::ranges::sort(circles, {}, &Circle::radius_);

(the {} in the middle stands for std::less, which is the default value for sorts).

Before that, from C++11 on, Boost HOF allows to approximate this by using proj:

using namespace boost::hof;

std::sort(begin(circles), end(circles), proj(&Circle::radius, _ < _));

Even if it is no longer useful for STL algorithms in C++20, proj is also compatible with any other libraries than the STL.

compose: passing the composition of several functions

C++ allows to pass functions around, but doesn’t allow to pass composition of functions around.

For example, consider those two functions:

int plusOne(int i)
{
    return i + 1;
}

int timesTwo(int i)
{
    return i * 2;
}

boost::hof::compose allows to pass the composition of those two functions:

auto const input = std::vector<int>{1, 2, 3, 4, 5};
auto results = std::vector<int>{};

std::transform(begin(input), end(input), back_inserter(results), boost::hof::compose(timesTwo, plusOne));

This allows to call those two functions successively on the inputs, without having to call the algorithm twice.

This particular example can also be implemented with C++20 range adaptors:

auto const input = std::vector<int>{1, 2, 3, 4, 5};

auto range = inputs
                | std::views::transform(plusOne)
                | std::views::transform(timesTwo);

auto result = std::vector<int>{range.begin(), range.end()};

But here too, compose can be used with other libraries than the STL.

Bonus: apply_eval: you shouldn’t have to use it, but just in case

In C++ the evaluation order of the arguments passed to a function is not specified. Relying on an evaluation order, for example from left to right, can lead to surprising results.

Now if you already have some code that depends on the evaluation order of its arguments, and if it is for example legacy code and it takes time to make it independent from the order, then to make it work until you fix it, apply_eval can guarantee an order of evaluation from left to right.

To illustrate, consider the following function taking two arguments:

g(f1(), f2());

f1 and f2 can be evaluated in any order. To constrain them to be evaluated from left to right, we can use apply_eval:

boost::hof::apply_eval(g, [](){ return f1(); }, [](){ return f2(); });

A rich library

Those are my favourite components from Boost HOF. There is also infix that we discuss in its own post.

But the library contains much more! If you’re interested in functional programming, then you should definitely check them out.

You will also like

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin