Think of Function Objects as Functions Rather Than Objects
The point of this article is to show why we should think of function objects as functions and not as objects, and what practical consequences this implies for writing code. This perspective is somewhat not natural at first sight, and there is a lot of code out there that doesn’t seem to treat function objects like functions.
By function objects, I mean objects of a class that has an operator()
. They are generally called functors when we write the class explicitly (the word functor also has other meanings, but let’s use it this way for this article shall we), and lambdas when we let the compiler write it for us.
In this article I use functors in the code examples, because it is perhaps less natural than lambdas to consider them as functions, so the guideline is less obvious to apply. After all, we write the words class
or struct
explicitly in code. But that guideline of considering function objects as functions also applies to lambdas.
Now is it worth it giving attention to functors? This is a whole topic in itself, but functors are not dead yet in modern C++, and for the developers still using C++03 , that’s all there is anyway.
To illustrate the point, let’s use the colorful example of a class representing a Ball
.
C++, more than an object oriented language
Our Ball
class has some ball data and some ball behaviour, including that it comes in various colors:
class Ball { public: Color getColor() const; ... };
Color
is defined to be an enum:
enum class Color { Red, Green, Blue, Yellow };
Let’s create a collection of balls:
std::vector<Ball> poolOfBalls;
And we want to pick the first ball of a given color. Say blue.
The algorithm to use here is std::find_if
, with a predicate checking whether a ball has the right color. We would want to use the same predicate for various colors to pick in case we later need another color than blue, and this can be achieved with a functor (in this case we’d actually rather use a lambda if possible. But let’s carry on with the example).
We may be tempted to think that a functor is an object. Indeed it is represented by a class, has constructors, methods (one or several overloads of operator()) and attributes. A lambda too, except the compiler writes it itself.
And in OO design, objects have names that represent what their responsibilities are. Here our functor is an object that checks whether the color is the one we want, so we may be tempted to call it something like ColorChecker, and write the following code:
class ColorChecker { public: explicit ColorChecker(Color color) : color_(color){} bool operator()(Ball const& ball) { return ball.getColor() == color_; } private: Color color_; };
And at call site:
blueBallPosition = std::find_if(balls.begin(), balls.end(), ColorChecker(Color::Blue));
After all, C++ is an object oriented language, so it seems only normal to use object oriented design practices, right ?
Except this is wrong. And the above is bad code.
In fact, seeing C++ as an OO language is restrictive. As explained by Scott Meyers in Effective C++ (item 1), C++ should be viewed as a confederation of languages, including OO, but also the STL (and also C and generic programming).
The STL does not follow the OO paradigm, but it rather follows the Functional Programming paradigm. And in Functional Programming, everything is a function.
Seeing a function object as a partially applied function
C++ functors are implemented with classes, methods and attributes, because these are the basic constructs of the language. But these are technical details that should not distract you from what a functor really is: a functor is a partially applied function.
In theory, our example of picking a ball of the right color could be thought of as repeatedly applying a function taking two parameters, a ball and a color, and checking whether the given ball has the given color:
bool hasColor(Ball const& ball, Color color) { return ball.getColor() == color; }
But in our case we want to check all balls with the same color, blue. So we could – still theorically – imagine partially applying hasColor
by passing Color::Blue
as a second parameter, resulting in a function hasColorBlue
that has only one parameter left to pass, that is the ball:
hasColorBlue = hasColor(Color::blue) // imaginary C++
And this resulting function’s implementation would be:
bool hasColorBlue(Ball const& ball) { return ball.getColor() == Color::Blue; }
Let’s note that this is in fact very similar to what std::bind
does, but for other reasons, we don’t want to use std::bind
– see Scott Meyer’s Effective Modern C++ (item 14).
Even though the above code was theoretical, this is how I think we should perceive functors and lambdas: partially applied functions. They are applied in two phases: the constructor that receives data, and the operator()
that uses that data plus some more passed to it directly.
Note that some functional languages such as Haskell natively allow partial application too, but with even more than two phases. Application in more than two phases is not native in C++, but can be emulated as shown in this article on currying by Vittorio Romeo.
Why does this all matter?
So far, this was pretty much all theory. How to perceive function objects in our mind, and how to see the C++ language as a whole. But what impact does this have on our code?
There is one practical consequence to benefit from these insights in your code, and this has to do with naming. The guideline here is to name a function object like you would name a function, and not like you would name an object. By doing this, their true nature of partially applied functions will appear in your code, and your code will fit better with the STL and be easier to read and maintain.
As often with naming, this is an easy fix that has low risk. But one that makes the code much clearer. In our cases the object name was ColorChecker
, but a function name is rather HasColor
. Let’s compare those two names:
Seeing a function object as an object:
blueBallPosition = std::find_if(balls.begin(), balls.end(), ColorChecker(Color::Blue));
Seeing a function object as a function:
blueBallPosition = std::find_if(balls.begin(), balls.end(), HasColor(Color::Blue));
Don’t you find that the second one reads more naturally?
Just for the pleasure of the eyes, let’s write it with a range algorithm taking the container directly:
blueBallPosition = find_if(balls, HasColor(Color::Blue));
In conclusion, name a function object like you would name a function, and not like you would name an object. The readers of your code will thank you for it.
Don't want to miss out ? Follow:   Share this post!