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.
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
3D Character Concept Art
Ryan McCombe
Ryan McCombe
Updated

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!

Troubleshooting: The function isn’t doing anything

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!

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!

Callables

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.

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 our main() function
  • The Player argument is provided later, in the Party type’s all_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.

Was this lesson useful?

Next Lesson

Lambdas

An introduction to lambda expressions - a concise way of defining simple, ad-hoc functions
3D Character Concept Art
Ryan McCombe
Ryan McCombe
Updated
Lesson Contents

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.

A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, Unlimited Access
Working with Functions
A computer programmer
This lesson is part of the course:

Professional C++

Comprehensive course covering advanced concepts, and how to use them on large-scale projects.

Free, unlimited access

This course includes:

  • 124 Lessons
  • 550+ Code Samples
  • 96% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Lambdas

An introduction to lambda expressions - a concise way of defining simple, ad-hoc functions
3D Character Concept Art
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved