std::invocable
, std::predicate
and std::function
.In this lesson, we’ll introduce some of the standard library utilities that can make working with the features we covered earlier in this chapter easier. We’ll cover:
std::invocable
concept lets us specify type requirements for any of the callables we’ve coveredstd::predicate
concept, lets us specify that our templates require a predicatestd::function
container provides a generalized way to store a callablestd::invocable
ConceptIn previous sections, when we had a function that receives another function as an argument, we’ve been fairly inelegant with how we specified that type.
We might just set the type to auto
, but this eliminates some type safety, and doesn’t document our expectations:
class Party {
public:
// This is too loose
bool all_of(auto Pred) {
return (
Pred(PlayerOne) &&
Pred(PlayerTwo) &&
Pred(PlayerThree)
);
}
};
Alternatively, we’ve specified the exact type of argument we expect, such as a function pointer:
class Player {
public:
bool isOnline() const { return true; }
};
class Party {
public:
using Handler = bool (*)(const Player&);
bool all_of(Handler Pred) {
return (
Pred(PlayerOne) &&
Pred(PlayerTwo) &&
Pred(PlayerThree)
);
}
private:
Player PlayerOne;
Player PlayerTwo;
Player PlayerThree;
};
struct OnlineChecker {
bool operator()(const Player& P) {
return P.isOnline();
}
};
int main() {
Party MyParty;
MyParty.all_of(OnlineChecker{});
}
error: 'bool Party::all_of(Party::Handler)': cannot convert argument 1 from 'OnlineChecker' to 'Party::Handler'
But this is unnecessarily restrictive. Our all_of()
function doesn’t care if we provide a function pointer, lambda, or functor. It will be happy to use either of them, so there’s no reason to deny consumers the choice.
As of C++20, the preferred way we specify type requirements is through concepts
The standard library has already provided a concept that can help us here. The std::invocable
concept is available after we include <concepts>
. To specify that a function template’s argument should be invocable, we would use it like this:
#include <iostream>
#include <concepts>
void Call(std::invocable auto Callback) {
Callback();
}
void Greet() { std::cout << "Hello From Function\n"; }
struct Greeter {
void operator()() {
std::cout << "Hello from Functor\n";
}
};
int main() {
Call(Greet);
Call(Greeter{});
Call([] { std::cout << "Hello From Lambda"; });
}
Hello From Function
Hello from Functor
Hello From Lambda
To specify the argument list of the required invocable, we would provide the types as template parameters to std::invocable
. Below, we require the first argument to Call
be an invocable that accepts two integer arguments:
#include <iostream>
#include <concepts>
void Call(std::invocable<int, int> auto Func) {
Func(1, 2);
}
void Function(int x, int y) {
std::cout << "Values received by function: "
<< x << ", " << y;
}
struct Functor {
void operator()(int x, int y) {
std::cout << "\nValues received by functor: "
<< x << ", " << y;
}
};
int main() {
Call(Function);
Call(Functor{});
Call([](int x, int y) {
std::cout << "\nValues received by lambda: "
<< x << ", " << y;
});
}
Below, we update our original all_of()
example to use this concept:
#include <concepts>
class Player {
public:
bool isOnline() const { return true; }
};
class Party {
public:
bool all_of(
std::invocable<const Player&> auto Pred) {
return (
P(PlayerOne) &&
P(PlayerTwo) &&
P(PlayerThree)
);
}
private:
Player PlayerOne;
Player PlayerTwo;
Player PlayerThree;
};
struct OnlineChecker {
bool operator()(const Player& P) {
return P.isOnline();
}
};
int main() {
Party MyParty;
MyParty.all_of(OnlineChecker{});
}
std::predicate
ConceptWhen our function is expected to return a bool
, or something convertible to a bool
, the standard library includes the std::predicate
concept to help us out:
#include <iostream>
#include <concepts>
void Call(std::predicate auto Func) {
std::cout << "That was a predicate";
}
void Call(auto Func) {
std::cout << "\nThat was not a predicate";
}
struct SomeType {};
int main() {
Call([] { return true; });
Call([] { return SomeType{}; });
}
That was a predicate
That was not a predicate
Similar to std::invocable
, we can provide the required argument list as template parameters. Below, we create a template where an argument is required to be a predicate that accepts an int
and a float
:
#include <concepts>
#include <iostream>
template <std::predicate<int, float> T>
void LogIfTrue(T Predicate, int x, float y) {
if (Predicate(x, y)) {
std::cout << "Predicate returned true";
}
}
int main() {
LogIfTrue([](int x, float y) {
return x == y;
}, 5, 5.0f);
}
Predicate returned true
Our earlier all_of()
function was expecting the callback to return a boolean, so we’d likely prefer to use std::predicate
in this scenario, rather than the less restrictive std::invocable
:
#include <concepts>
struct Player {};
class Party {
public:
template <std::predicate<Player&> T>
bool all_of(T Predicate){
return (
Predicate(PlayerOne) &&
Predicate(PlayerTwo) &&
Predicate(PlayerThree)
);
}
};
The standard library currently doesn’t provide a concept that requires any return type other than boolean, using std::predicate
. If we need this, we can define our own. Below, we create a concept for a function that accepts two int
objects and returns an int
:
#include <iostream>
#include <concepts>
template <typename T>
concept TwoIntsToInt = requires(T Callable) {
{ Callable(1, 1) } -> std::same_as<int>;
};
void Call(TwoIntsToInt auto Func, int x, int y) {
std::cout << "The callback returned: "
<< Func(x, y);
}
int main() {
Call([](int x, int y) {
return x + y;
}, 1, 2);
}
The callback returned: 3
Below, we create a template that requires a callable return an int
, and for the argument list to be provided as template parameters. This effectively replicates the behavior of std::predicate
except, where std::predicate
requires the function return a bool
, our IntReturner
concept requires an int
be returned.
Note: The ...
syntax in the following example is a parameter pack, which is how we represent a variable number of parameters. We cover this in more detail later in this chapter in our lesson on variadic functions.
#include <iostream>
#include <concepts>
template<typename T, typename... Args>
concept IntReturner =
requires(T Func, Args... args) {
{ Func(args...) } -> std::same_as<int>;
};
template <IntReturner<int, float, double> T>
void Call(T Func, int x, float y, double z) {
std::cout << "The callback returned: "
<< Func(x, y, z);
}
int main() {
Call([](int x, float y, double z) -> int {
return x + y + z;
}, 1, 2.0f, 3.0);
}
The callback returned: 6
std::function
ContainerThe std::function
container within the <functional>
header gives us a generalized and flexible way to represent callables such as lambdas, functors, or function pointers. If we had a callable that returns void
and has no arguments, we could store it as a variable called Example
using the following syntax:
#include <functional>
std::function<void()> Example;
Below, we create a container for storing a callable that receives two int
arguments, and returns a bool
:
#include <functional>
std::function<bool(int, int)> Example;
As with most containers, when we’re providing an initial value to our std::function
, we do not need to provide the template arguments. The compiler can automatically infer the type required, based on the type of object we’re initializing the container with.
This technique is called class template argument deduction, or CTAD:
#include <functional>
bool Function(int x, int y) { return true; };
int main() {
std::function Callable{Function};
}
std::function
?The main benefit of std::function
is similar to the benefit of std::invocable
- that is, it does not prescribe the specific type of callable, thereby giving us more flexibility. Below, we create a std::function
that first stores a function pointer, then a functor, then a lambda:
#include <functional>
void Function(){};
struct Functor{
void operator()(){}
};
int main() {
std::function Callable{Function};
Callable = Functor{};
Callable = [] {};
}
std::function
containersA std::function
container can be empty. We can check for this by coercing it to a boolean:
#include <iostream>
#include <functional>
int main() {
std::function<void()> Callable;
if (!Callable) {
std::cout << "Callable is empty";
}
Callable = [] {};
if (Callable) {
std::cout << "...but not any more!";
}
}
Callable is empty...but not any more!
We can assign nullptr
to a std::function
, effectively setting it to the empty state:
#include <functional>
#include <iostream>
int main() {
std::function Callable{[] {}};
if (Callable) {
std::cout << "A function is stored";
}
Callable = nullptr;
if (!Callable) {
std::cout << "...but not any more!";
}
}
A function is stored...but not any more!
std::function
as a member variableThe main place we’ll see std::function
being used is within class definitions. Below, we create a Player
class that allows the outside world to provide a callback to be invoked when our Player
is defeated, which it stores in a std::function
.
#include <functional>
#include <iostream>
class Player{
public:
Player(std::invocable auto DefeatCallback)
: OnDefeat { DefeatCallback } {}
void TakeDamage() {
Health -= 50;
if (Health <= 0 && OnDefeat) {
OnDefeat();
}
}
private:
int Health{100};
std::function<void()> OnDefeat;
};
int main() {
Player PlayerOne {[] {
std::cout << "The player was defeated";
}};
PlayerOne.TakeDamage();
PlayerOne.TakeDamage();
}
The player was defeated
The previous example shows the basis of a powerful design pattern called observers. Our Player
object supports observers, as it gives other code the option to subscribe to specific events. This is done by the other code providing a callable to Player
, and the Player
class then calling it when the event happens.
This allows those objects to react to those events, even if they were not responsible for causing them. Our previous simple example only allows one observer, but this can be expanded.
For example, the Player
class could store an array of std::function
objects, and provide methods such as Subscribe()
and Unsubscribe()
to add and remove functions to that collection. Then, when the event happens, we could iterate over that array and invoke all the callbacks, thereby notifying every observer.
std::function
vs std::invocable
A common question relates to the difference between std::function
and std::invocable
. Their use cases seem similar, and the decision mostly comes up when defining function parameter types:
void A(std::function<void(int, int)> F) {}
void B(std::invocable<int, int> auto F) {}
The key difference is that std::function
is for storing callables, whilst std::invocable
is for checking if something is callable. std::invocable
, like any concept, is designed for analyzing types, runs at compile time, and has no run-time performance cost.
std::function
can provide an almost similar level of type safety at compile time but, at run time, it also creates an object to wrap the function. When our callable is a simple function argument, for example, creating this intermediate object is rarely useful.
So, if we’re just using std::function
for type safety and documentation, we should consider using concepts such as std::invocable
or std::predicate
instead.
When writing functional code, we’ll commonly need to create very simple callables that accept two arguments and apply some basic operator on them. The standard library contains a collection of types that can help us create functors that implement these operators.
Everything we cover in this section is within the <functional>
header:
#include <functional>
A common requirement we’ll have when sorting a container, for example, is to provide a function specifying how we want the container to be sorted. Typically, these functions will accept two arguments and should return true
if the first argument comes before the second in our sort order.
Below, we sort a forward list in ascending order, by providing a lambda that accepts two arguments, x
and y
, and returns the result of x < y
:
#include <iostream>
#include <forward_list>
int main() {
std::forward_list Nums{3, 1, 2, 5, 4};
Nums.sort([](int x, int y) { return x < y; });
std::cout << "Sorted Container: ";
for (auto Num : Nums) {
std::cout << Num << ", ";
}
}
Sorted Container: 1, 2, 3, 4, 5,
The standard library can help us here - a functor that accepts two arguments and returns the result of using the <
operator with them is available by instantiating std::less
. As such, we didn’t need to write the lambda - we can just do this:
#include <forward_list>
#include <iostream>
#include <functional>
int main() {
std::forward_list Nums{3, 1, 2, 5, 4};
Nums.sort(std::less{});
std::cout << "Sorted Container: ";
for (auto Num : Nums) {
std::cout << Num << ", ";
}
}
Sorted Container: 1, 2, 3, 4, 5,
A full suite of types for creating functors that accept two arguments and return the result of a comparison operator is available:
std::less
uses the <
operatorstd::less_equal
uses the <=
operatorstd::equal_to
uses the ==
operatorstd::not_equal_to
uses the !=
operatorstd::greater
uses the >
operatorstd::greater_equal
uses the >=
operatorAn algorithm like fold_left_first()
accepts a collection of objects, and returns a single object. The single object is generated by folding the collection together - that is, combining two objects at a time until only one object remains.
We provide a function to specify how objects are combined. Below, we provide a lambda that adds the objects together. The net result of this is that the algorithm returns the sum of every number in our collection:
#include <vector>
#include <iostream>
#include <algorithm>
int main() {
std::vector Nums{1, 2, 3, 4, 5};
auto Sum{std::ranges::fold_left_first(
Nums, [](int x, int y) { return x + y; })};
std::cout << "Sum: " << Sum.value_or(0);
}
Sum: 15
We cover fold algorithms in more detail later in the course. For now, the key point is that the standard library provides types for creating functors that implement basic arithmetic operators like +
. As such, we could replace the lambda in our previous example with an instance of std::plus
:
#include <vector>
#include <iostream>
#include <algorithm>
int main() {
std::vector Nums{1, 2, 3, 4, 5};
auto Sum{std::ranges::fold_left_first(
Nums, std::plus{})};
std::cout << "Sum: " << Sum.value_or(0);
}
Sum: 15
The full collection of function objects for implementing arithmetic operations are:
std::plus
uses the binary +
operatorstd::minus
uses the binary -
operatorstd::multiplies
uses the binary *
operatorstd::divides
uses the /
operatorstd::modulus
uses the %
operatorWe also have std::negate
, which uses the unary -
operator. For example, a functor created from std::negate
will accept a single argument x
and will return -x
.
Less commonly, the standard library includes functors that implement the bitwise operators. We covered these operators in more detail in the previous course:
Below, we use std::bit_or
to create a functor that implements the bitwise or operator |
:
#include <vector>
#include <iostream>
#include <algorithm>
#include <functional>
#include <bitset>
std::bitset<3> FLAG_ONE {1 << 0};
std::bitset<3> FLAG_TWO {1 << 1};
std::bitset<3> FLAG_THREE {1 << 2};
int main() {
std::vector Bits{FLAG_ONE, FLAG_THREE};
std::bitset Bitset{
std::ranges::fold_left_first(
Bits, std::bit_or{}
).value_or(0)};
std::cout << "FLAG_ONE "
<< (Bitset[0] ? "is" : "is not")
<< " set\n";
std::cout << "FLAG_TWO "
<< (Bitset[1] ? "is" : "is not")
<< " set\n";
std::cout << "FLAG_THREE "
<< (Bitset[2] ? "is" : "is not")
<< " set\n";
}
FLAG_ONE is set
FLAG_TWO is not set
FLAG_THREE is set
The full collection includes:
std::bit_or
which uses the |
operatorstd::bit_and
which uses the binary &
operatorstd::bit_xor
which uses the ^
operatorWe also have std::bit_not
, which uses the unary ~
operator. For example, a functor created from std::bit_not
will accept a single argument x
and will return ~x
.
In this lesson, we've explored function helpers available in the standard library. This included concepts like std::invocable
and std::predicate
, the std::function
container, and the suite of standard library functors for implementing basic operations.
std::invocable
concept for specifying callable type requirements.std::predicate
concept for functions expected to return a boolean.std::function
for storing callable objects, providing flexibility across different callable types.std::function
usage.std::function
versus std::invocable
and when to use each.A comprehensive overview of function helpers in the standard library, including std::invocable
, std::predicate
and std::function
.
Comprehensive course covering advanced concepts, and how to use them on large-scale projects.