Much of the content of this series relates to a programming concept known as first-class functions.
A programming language supports first-class functions if it allows functions to be treated like any other type of data. For example:
C++ supports first-class functions in several different ways. In this chapter, we will cover the three main options we have: function pointers, function objects, and lambdas
But first, let's see a scenario where first-class functions are a useful feature
Let’s imagine we have a Party
class, which is what enables our Player
objects to group up and take on bigger challenges.
class Player {
public:
bool isAlive() const { return true; }
};
class Party {
public:
Player PlayerOne;
Player PlayerTwo;
Player PlayerThree;
};
Elsewhere in our code, we’ll want to find out more information about this party. For example, we might want to check that everyone is alive:
#include <iostream>
class Player {/*...*/};
class Party {/*...*/};
int main() {
Party MyParty;
bool isEveryoneAlive {
MyParty.PlayerOne.isAlive() &&
MyParty.PlayerTwo.isAlive() &&
MyParty.PlayerThree.isAlive()
};
if (isEveryoneAlive) {
std::cout << "Everyone is alive";
}
}
Everyone is alive
Aside from requiring quite a lot of code, this implementation also makes a big assumption - it assumes the party will always have exactly 3 members. In reality, there may be fewer, and we’d want to manage which Player
slots are used within the Party
class, rather than making external code responsible.
Worse, if we later expand our Party
class to allow additional characters, code like this may still compile, but return the incorrect result because it’s not checking if the new PlayerFour
is alive.
The root cause here is that our Party
isn’t encapsulated properly. External code shouldn’t be able to poke around the internal workings of our objects like this.
The Party
class should take care of details like iterating over its members, and simply expose a friendly public method to access that behaviour.
We covered the importance of encapsulation, and how to implement it, in our beginner course:
Lets implement this isEveryoneAlive()
function as a class method. Let's also make all of our Player
members private to make our interface simpler.
Then, our code can just call MyParty.isEveryoneAlive()
:
#include <iostream>
class Player {/*...*/};
class Party {
public:
bool isEveryoneAlive() {
return PlayerOne.isAlive() &&
PlayerTwo.isAlive() &&
PlayerThree.isAlive();
}
private:
Player PlayerOne;
Player PlayerTwo;
Player PlayerThree;
};
int main() {
Party MyParty;
if (MyParty.isEveryoneAlive()) {
std::cout << "Everyone is alive";
}
}
Everyone is alive
That is better, but we’ve solved this problem in a very specific way - we’re only allowing users of our class to determine if everyone is alive. There are many possible questions people will want to ask of our party.
For example, the outside world might need to know if everyone in the party is online, or above a specific level, or to check if our party has someone who can fill a specifc role:
MyParty.isEveryoneOnline();
MyParty.isEveryoneAtLeastLevel(50);
MyParty.isAnyoneAHealer();
We can’t predict everything people might want to use our class for, and we shouldn’t need to write code for all of those anyway. Thankfully, this is one of the problems first-class functions were designed to solve.
To solve this problem more flexibly, let's first create a standalone function that implements the check we want to do on each member of the party:
bool PlayerIsAlive(const Player& P) {
return P.isAlive();
}
A function like this that receives an argument, and returns a boolean representing whether or not that argument satisfies a specific condition is sometimes called a predicate. In this example, our predicate is met if the Player
we pass as an argument is alive.
To run this check on everyone in the party, we’d like some way to pass this function to a method on the Party
class. This method could then invoke this function for every Player
in our Party
, and return true
if every invocation of this predicate returned true
.
For example, a method that checks if every Player
meets some condition could be called all_of()
and, to check if everyone in our Party
is alive, the expression might look something like MyParty.all_of(PlayerIsAlive)
:
#include <iostream>
class Player {/*...*/};
class Party {
public:
bool all_of(auto Predicate) {
return Predicate(PlayerOne) &&
Predicate(PlayerTwo) &&
Predicate(PlayerThree);
}
private:
};
bool PlayerIsAlive(const Player& P) {
return P.isAlive();
}
int main() {
Party MyParty;
if (MyParty.all_of(PlayerIsAlive)) {
std::cout << "Everyone is alive";
}
}
Everyone is alive
The previous example was accomplished using a function pointer, which we cover in more detail in the next lesson.
The beauty of this design is that we can now ask anything of our Party
, without the class needing to be expanded or modified.
The outside world provides the function they want to call, whilst our class can handle the iteration, ensuring that function is called for every Player
it is managing:
#include <iostream>
class Player {/*...*/};
class Party {/*...*/};
bool PlayerIsAlive(const Player& P) {
return P.isAlive();
}
bool PlayerIsOnline(const Player& P) {
return P.isOnline();
}
bool MinLevel50(const Player& P) {
return P.GetLevel() >= 50;
}
int main() {
Party MyParty;
if (MyParty.all_of(PlayerIsAlive)) {
std::cout << "Everyone is alive";
}
if (MyParty.all_of(PlayerIsOnline)) {
std::cout << "\nEveryone is online";
}
if (!MyParty.all_of(MinLevel50)) {
std::cout << "\nNot everyone is level 50+";
}
}
Everyone is alive
Everyone is online
Not everyone is level 50+
A function that is provided to another function in this way is sometimes referred to as a callback. In our previous example, PlayerIsAlive()
, PlayerIsOnline()
and MinLevel50()
are all used as callbacks.
They’re provided to another function - the all_of()
method in this case. For all_of()
to complete its work and generate its return value, it uses our callbacks as needed.
Over the coming lessons, we will see more scenarios where we can apply designs like this and some additional ways they can be implemented in C++
In this lesson, we used function pointers, which are only one of several possibilities. We cover function pointers in more detail in the next lesson.
In this lesson, we introduced the concept of first-class functions, and one of the ways in which C++ allows us to implement this design. The key things we learned include:
all_of()
method demonstrated some of the flexibility that first-class functions can provide.Learn about first-class functions in C++: a feature that lets you store functions in variables, pass them to other functions, and return them, opening up new design possibilities
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.