Function Objects (Functors)
This lesson introduces function objects, or functors. This concept allows us to create objects that can be used as functions, including state management and parameter handling.
Another option we have for implementing first-class functions in C++ is function objects. Function objects are sometimes also referred to as functors.
Creating functors involves operator overloading. We've previously seen how we can overload operators like ++
in our custom class code.
The syntax we use to call functions, ()
, is also an operator, and so can be overloaded in the same way:
class Functor {
public:
void operator()() const {
std::cout << "Hello from Functor!";
}
};
The double set of ()
in this function name might seem weird, but it is consistent with the syntax we use for other operator overloads. For example, we'd overload ++
like this:
void operator++() {};
So, given we're overloading the ()
operator, we can imagine replacing the ++
in this function name with ()
, to give operator()()
When our type overloads the ()
operator, we can use it with objects of that type, creating a function-like interaction:
#include <iostream>
class Functor {
public:
void operator()() const {
std::cout << "Hello from Functor!";
}
};
int main() {
Functor MyFunctor;
MyFunctor();
}
Hello from Functor!
Using Functors as First-Class Functions
Like any object, a functor can be passed to other functions, as either a copy or a reference. Therefore, functors allow us to implement the behavior of first-class functions:
#include <iostream>
class Functor {/*...*/};
void CallIfEven(int n, auto Func) {
if (n % 2 == 0) {
std::cout << "n is even, so calling Func:\n";
Func();
};
}
int main() {
Functor MyFunctor;
CallIfEven(2, MyFunctor);
}
n is even, so calling Func:
Hello from Functor!
Functor Return Values
As with regular functions, we can return values from functors. We do that by replacing the void
in our overload with the type we want to return, and then using appropriate return
statements:
#include <iostream>
class Functor {
public:
int operator()() const { return 5; }
};
int main() {
Functor MyFunctor;
std::cout << "MyFunctor returns: "
<< MyFunctor();
}
MyFunctor returns: 5
Functor Parameters
Within the second set of brackets, we can allow our functors to accept arguments. The syntax and capabilities here are exactly the same as they are when providing parameter lists to any other type of function:
#include <iostream>
class Functor {
public:
int operator()(int x, int y) const {
return x + y;
}
};
int main() {
Functor MyFunctor;
std::cout << "MyFunctor(2, 3) returns: "
<< MyFunctor(2, 3);
}
MyFunctor(2, 3) returns: 5
This also means we can overload the ()
operator multiple times within the same class, as long as our parameter lists are unique:
#include <iostream>
class Functor {
public:
void operator()() const {
std::cout << "Hello from Functor\n";
}
void operator()(int x) const {
std::cout << "Hello Integer\n";
}
};
int main() {
Functor MyFunctor;
MyFunctor();
MyFunctor(5);
}
Hello from Functor
Hello Integer
Functor Benefits
Functors have a lot more power than simple function pointers. Being instances of classes, we naturally have all the power that brings with it. For example:
- they can have other methods and variables accessible with the
.
operator - they can have user-defined constructors
- they can overload other operators
- they can exist within an inheritance tree
The most common use case for functors is simply when we require a callable with some persistent state. For example, below, we have a functor that keeps track of how many times it has been called:
#include <iostream>
class Functor {
public:
void operator()() {
std::cout << "I have been called "
<< ++Invocations
<< " time(s)\n";
}
private:
int Invocations { 0 };
};
int main() {
Functor MyFunctor;
MyFunctor();
MyFunctor();
MyFunctor();
}
I have been called 1 time(s)
I have been called 2 time(s)
I have been called 3 time(s)
Implementing similar behavior with a regular function wouldn't be quite so easy to encapsulate.
Passing Functors by Reference
Remember, as with any object, a functor passed to another function is going to be passed by value by default.
Here, our CallIfEven()
function is receiving copies of our function object:
void CallIfEven(int n, auto Func) {
if (n%2 == 0) Func();
}
When our reason for using the functor is to maintain some internal state, this is generally not what we want to happen.
In the following example, note our functor doesn't accurately track how many times it is being called. This is because the CallIfEven()
function is acting on a copy of our functor:
#include <iostream>
class Functor {/*...*/};
void CallIfEven(int n, auto Func) {
if (n % 2 == 0) Func();
}
int main() {
Functor MyFunctor;
MyFunctor();
CallIfEven(2, MyFunctor);
MyFunctor();
}
I have been called 1 time(s)
I have been called 2 time(s)
I have been called 2 time(s)
Instead, we typically want to pass our functors by reference, by appending &
to the type in the usual way:
#include <iostream>
class Functor {/*...*/};
void CallIfEven(int n, auto& Func) {
if (n % 2 == 0) Func();
}
int main() {/*...*/}
I have been called 1 time(s)
I have been called 2 time(s)
I have been called 3 time(s)
Using Functors for Partial Application
In the previous lesson, we introduced the following scenario, where we had a function that checks if every Player
object in a collection is at least level 40
:
#include <iostream>
class Player {/*...*/};
class Party {/*...*/};
bool MinLevel40(const Player& P) {
return P.GetLevel() >= 40;
}
int main() {
Party MyParty;
if (MyParty.all_of(MinLevel40)) {
std::cout << "Everyone is level 40 or above";
}
}
Everyone is level 40 or above
The MinLevel40
function is a bit inflexible - ideally, we'd like to be able to provide the minimum Level
as an argument, rather than fixing it to 40
. However, we're also not able to provide the MinLevel
and Player
at the same time, as those arguments are stored in different places in our code.
We showed how we could solve this using a template function in the previous lesson:
#include <iostream>
class Player {/*...*/};
class Party {/*...*/};
template <int Min>
bool MinLevel(const Player& P) {
return P.GetLevel() >= Min;
}
int main() {
Party MyParty;
if (MyParty.all_of(MinLevel<40>)) {
std::cout << "Everyone is level 40 or above";
}
if (!MyParty.all_of(MinLevel<50>)) {
std::cout << "\nBut not level 50 or above";
}
}
Everyone is level 40 or above
But not level 50 or above
That approach works, but template parameters must be known at compile time. With our new knowledge of functors, we now have a way we could implement this at run time:
#include <iostream>
class Player {/*...*/};
class Party {/*...*/};
struct LevelChecker {
int MinLevel {50};
bool operator()(const Player& P) {
return P.GetLevel() >= MinLevel;
}
};
int main() {
Party MyParty;
if (MyParty.all_of(LevelChecker{40})) {
std::cout << "Everyone is level 40 or above";
}
if (!MyParty.all_of(LevelChecker{50})) {
std::cout << "\nBut not level 50 or above";
}
}
Everyone is level 40 or above
But not level 50 or above
In this example, we can imagine we have a function that requires two arguments, but we're not able to provide them at the same time or place:
- The
MinLevel
argument is provided in ourmain()
function - The
Player
argument is provided later, in theParty
type'sall_of()
method
We solved this using partial application. By instantiating our LevelChecker
type, we create a functor. That callable is created by providing one of the parameters it needs - the MinLevel
- so it is partially applied.
It's not fully applied because it still needs one more argument - the Player
- to complete its work. That argument is provided later, elsewhere in our code.
A functor is only one way of implementing this design. We cover partial application in a dedicated lesson later in the chapter.
Summary
In this lesson, we introduced function objects, or functors, which are objects that can be used like functions. The main points we learned include:
- Function objects, also known as functors, are objects that can be called like functions thanks to the overloading of the
()
operator. - Operator overloading enables functors to not only mimic function calls but also accept parameters and return values.
- Functors can be used to implement first-class functions, as they can passed to and returned from functions like any other object.
- Functors can have other member functions and variables, benefit from user-defined constructors, overload other operators, and participate in inheritance hierarchies.
- A common use case for functors is to maintain persistent state across calls, which we showcased by counting invocations.
- When passing functors to functions, passing by reference (using
&
) is crucial for maintaining state across calls. - Functors offer a solution for partial application, enabling arguments to be provided at different times or places in the code, thus allowing for more flexible function invocation patterns.
Lambdas
An introduction to lambda expressions - a concise way of defining simple, ad-hoc functions