This is my learning notes from the course - Mastering C++ Standard Library Features.
Storing Callable Objects
- Storing function objects and closures in variables
- Interactions between lambdas and function pointers
- The
FunctionObject
andCallable
concepts std::function
: Type erasure forCallable
objects
Storing Functions in Variables
1 | void foo() { std::cout << "hello!\n"; } |
- Functions can be stored via function pointers
1
2
3
4
5auto p0 = &foo;
void(*p1)() = &foo;
p0(); // prints "hello!"
p1(); // prints "hello!"1
std::vector<void(*)()> vec{&foo};
Storing Hand-written Function Objects in Variables
- The type of hand-written function objects is known
1
struct foo { void operator(){ std::cout << "hello!\n"; } };
1
2
3
4
5auto f0 = foo{};
foo f1;
f0(); // prints "hello!"
f1(); // prints "hello!"1
std::vector<foo> vec{foo{}, foo{}, foo{}};
Storing Closures in Variables
- Lambda expressions produce closures of anonymous types
auto
(or templates) are required to store closures1
auto p0 = []{ std::cout << "hello!\n"; };
1
2
3
4template <typename F>
struct wrapper { F _f; };
wrapper<decltype(p0)> w{std::move(p0)};- Even though multiple lambda expressions might have the same signature (same parameters, captures, and return type), it is not possible to store them homogeneously without type erasure
1
2auto l0 = []{};
auto l1 = []{};decltype(l0)
is different fromdecltype(l1)
Lambda Expressions and Function Pointers
Lambda expressions are implicitly convertible to function pointers as long as they do not capture anything and have the same signature
1
2
3
4
5auto l0 = []{};
auto l1 = []{};
void(*p0)() = l0;
p0 = l1;- This is a lightweight form of type erasure
Example of what happens if we try to assign a lambda captured in something to a function pointer
1
2
3
4int i;
auto l0 = [&i]{ };
void(*p0)() = l0;ERROR: cannot convert
main()::<lambda()>
tovoid(*)()
in initializationStateless lambda expressions can be explicitly converted into function pointers by using the unary + operator
1
2
3
4
5auto l0 = []{ };
static_assert(!std::is_same_v<decltype(l0), void(*)()>);
auto p0 = +[]{ };
static_assert(std::is_same_v<decltype(p0), void(*)()>);- This makes usage of auto possible and prevents spelling out complicated function pointer type
The Function Object and Callable Concepts
- Standard Library classes “satisfy“ one or more concepts
- A “concept“ is a set of requirements that a type must follow
- The requirements are both syntactic and semantic
- DefaultConstructible: https://en.cppreference.com/w/cpp/named_req/DefaultConstructible
- FunctionObject: https://en.cppreference.com/w/cpp/named_req/FunctionObject
- Callable: https://en.cppreference.com/w/cpp/named_req/Callable
std::invoke
: https://en.cppreference.com/w/cpp/utility/functional/invoke1
2
3
4
5
6
7
8
9
10
11std::invoke(add, -9, 23);
std::invoke([]() { print(42); });
Foo foo(1234);
std::invoke(&Foo::print, foo);
int num = std::invoke(&Foo::_num, foo);
assert(num == 1234);
std::invoke(printer{}, 18);- Very useful in generic code and templates
- Many utilities in the Standard Library accept
Callable
objects instead ofFunctionObjects
to be more general/flexible
Type Erasure
- Type erasure is the idea of “erasing“ the concrete type of an object into some common type that can store anything satisfying a particular concept
- Usually requires indirection and/or dynamic allocations
1
2
3
4
5struct A { void foo(); };
struct B { void bar(); };
std::any x = A{};
x = B{};
std::function
: Type Erasure for Callable Objects
The Standard Library provides a type erasing wrapper for
Callable
objects:std::function
It is a template class that takes a function signature as a parameter
An instance of
std::function
can then store anyCallable
matching that particular signatureExample:
1
2
3
4
5
6
7int add(int a, int b) { return a + b; }
const auto sub = [](int a, int b) { return a + b; };
struct mult
{
int operator()(int a, int b) const { return a * b; }
};1
2
3
4std::function<int(int, int)> f = add;
f = sub;
f = mult{};
f = [](int a, int b){ return a / b; };std::function
: https://en.cppreference.com/w/cpp/utility/functional/functionSome examples of what you can store in
std::function
1
2
3// non-member functions
std::function<void(int)> f = print_num;
f(-9);1
2
3// lambda expressions
std::function<void()> f = []{ std::cout << 42; };
f();1
2
3
4// member functions
std::function<void(const Foo&, int)> f = &Foo::print;
Foo foo(1234);
f(foo, 1);1
2
3// data members (uncommon)
std::function<int(const Foo&)> f = &Foo::_number;
std::cout << f(foo) << "\n";1
2
3// handwritten function objects
std::function<void(int)> f = printer{};
f(1234);std::function
can be “empty“ and can be arbitrary rebound1
2int add(int a, int b);
int sub(int a, int b);1
2
3
4std::function<int(int, int)> f; // <-- empty
f = add; // <-- now stores `add`
f = sub; // <-- now stores `sub`
f = nullptr; // <-- now empty againstd::function
can be easily stored in container1
2
3
4
5struct button
{
std::vector<std::function<void()>> on_click;
// ...
};button b; b += []{ open_modal(1) }; b += []{ std::cout << "opening modal ... \n"; };
std::function
is not a zero-cost abstraction- Invoking the stored object requires indirection
- Can dynamically allocate if the stored object is big
- Hard to inline/optimize
Summary
- Understood that functions can be stored in variables using function pointers
- Learned that the hand-written function objects can be easily stored as their type is known
- Learned that closures must be stored with
auto
as their type is “anonymous“ - Studied that the unary operator
+
can be used to explicitly produce function pointers from stateless lambda expressions - Learned about the Standard Library provides two concepts:
FunctionObject
andCallable
- Understood that
Callable
is more general as it includes member function pointers and member data pointers std::invoke
can be used to invoke anyCallable
object- Looked at many Standard Library utilities that accept
Callable
objects for genericity std::function
is a general-purpose polymorphicCallable
wrapper- It supports copyable
Callable
objects - It uses type erasure and can potentially allocate memory at run-time
- It is copyable, can be stored in containers, can be “empty”, and can be arbitrary rebound to other
Callable
objects at run-time - Guidelines:
- Never use
std::function
unless you need type erasure - Use
auto
and templates instead - they can refer to arbitraryCallable
objects without type erasure - Use
std::function
when you need flexibility at run-time or need to storeCallable
objects with the same signature but different types homogeneously
- Never use
- It supports copyable