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!
A common mistake when working with functors is to use ()
operator with the type, rather than an object of that type. For example:
#include <iostream>
class Functor {/*...*/};
int main() {
Functor();
}
This code will compile successfully, but it is not calling operator()()
. Rather, it is instantiating our Functor
class to create an object.
To use the operator, we need to use the ()
syntax on an instance of the class, rather than the class. We can instantiate the class, and then immediately call the ()
operator on the object returned from that instantiation, if we wish:
#include <iostream>
class Functor {/*...*/};
int main() {
Functor()();
}
Hello from Functor!
When doing this, we’d typically replace the first set of parenthesis in the previous expression with {}
instead, making it more obvious we’re initializing an object:
#include <iostream>
class Functor {/*...*/};
int main() {
Functor{}();
}
Hello from Functor!
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!
A function object might look a lot like a function, particularly given it is "callable" using the same code. But, it’s not strictly correct to refer to it as a function.
Things that can be "called" (or invoked) in programming languages are often referred to as "callables". Functions are an example of callables, but not all callables are functions.
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
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
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:
.
operatorThe 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.
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)
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:
MinLevel
argument is provided in our main()
functionPlayer
argument is provided later, in the Party
type’s all_of()
methodWe 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.
In this lesson, we introduced function objects, or functors, which are objects that can be used like functions. The main points we learned include:
()
operator.&
) is crucial for maintaining state across calls.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.
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.