Standard Library Function Helpers
A comprehensive overview of function helpers in the standard library, including 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:
- The
std::invocable
concept lets us specify type requirements for any of the callables we've covered - The more specific
std::predicate
concept, lets us specify that our templates require a predicate - The
std::function
container provides a generalized way to store a callable - Some standard library functors allow us to quickly create some commonly needed callables
The std::invocable
Concept
In 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
Concepts in C++20
Learn how to use C++20 concepts to constrain template parameters, improve error messages, and enhance code readability.
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{});
}
The std::predicate
Concept
When 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)
);
}
};
Concepts for Other Return Types
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
The std::function
Container
The 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;
Class Template Argument Deduction
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};
}
Why do we need 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 = [] {};
}
Empty std::function
containers
A 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!
Using std::function
as a member variable
The 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
Standard Library Function Objects
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>
Comparison Operators
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>=
operator
Arithmetic Operators
An 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%
operator
We 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
.
Bitwise Operators
Less commonly, the standard library includes functors that implement the bitwise operators. We covered these operators in more detail in the previous course:
Bitwise Operators and Bit Flags
Unravel the fundamentals of bitwise operators and bit flags in this practical lesson
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^
operator
We 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
.
Summary
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.
Key Takeaways
- Introduction to the
std::invocable
concept for specifying callable type requirements. - Understanding the
std::predicate
concept for functions expected to return a boolean. - Demonstrated how to create custom concepts for functions with specific return types beyond boolean.
- How to use
std::function
for storing callable objects, providing flexibility across different callable types. - The use of class template argument deduction (CTAD) in simplifying
std::function
usage. - Discussed the benefits and usage scenarios of
std::function
versusstd::invocable
and when to use each. - Exploration of standard library function objects for implementing comparison, arithmetic, and bitwise operators, reducing the need to write our own.
Pointers to Members
Learn how to create pointers to class functions and data members, and how to use them