diff --git a/book/src/ch14-callables-and-type-erasure.md b/book/src/ch14-callables-and-type-erasure.md new file mode 100644 index 0000000..20f7ece --- /dev/null +++ b/book/src/ch14-callables-and-type-erasure.md @@ -0,0 +1,112 @@ +# Callables and type erasure + +## What is a callable +A callable is any entity that can be used after the function‑call operator `()`. The C++ standard groups several kinds under this umbrella. A function pointer points to a free function and can be invoked directly. A function object, also called a functor, is a class type that overloads `operator()`. Lambdas are unnamed function objects that the compiler synthesises from a capture list and a body. `std::function` is a type‑erased wrapper that can hold any of the previous forms provided the call signature matches. Finally a pointer to member function or a pointer to data member also qualifies as a callable when used together with an object instance. + +The generic algorithms introduced in chapter 12 and the range algorithms of chapter 13 accept a callable as a parameter. The parameter is written as a template type that satisfies the *invocable* requirement. This design allows the same algorithm to work with a raw function pointer, a lambda that captures state, or a `std::function` supplied by the caller. The concept is central to generic programming because it decouples the algorithm from the concrete call target. + +## Lambdas +Lambdas provide a concise way to create a function object. The syntax starts with a capture list in square brackets, followed by an optional parameter list, an optional mutable specifier, an optional exception specification, and a body. The capture list determines which surrounding variables become members of the closure type. + +* `[x]` copies the variable `x` into the closure. The copy is a separate object. Later modifications of the original x do not affect the captured value. +* `[&x]` stores a reference to `x`. The closure can read and write the original variable through the reference. +* `[=, this]` captures all automatic variables that appear in the body by copy and also captures the current object `*this` by reference. This form is useful inside a non‑static member function when the lambda needs to read both data members and local variables. +* `[]` declares a capture‑less lambda. Such a lambda has no state and can be converted to a function pointer if it does not use `this`. + +A lambda can be declared `mutable` to allow modification of its captured copies. Without `mutable` the `operator()` of the closure is const, which forbids changing any captured by copy. Since C++20 a lambda can introduce explicit template parameters using the abbreviated syntax `[](T x) { return x }`. The compiler treats this as a generic lambda that can be instantiated with any type `T` that satisfies the body. + +The following example demonstrates a lambda used as a predicate for `std::ranges::count_if`. The program builds a vector of integers, counts the elements that are even, and prints the result. The lambda captures nothing and therefore has the type `bool(int) const`. + +```cpp +{{#include ../../examples/ch14/lambda_predicate.cpp}} +``` + +The test harness checks that the program prints the line `count = 3`. The lambda illustrates how a small, capture‑less callable integrates seamlessly with a range algorithm. + +### Capturing state +When a lambda captures a variable by value, the closure stores its own copy. This makes the lambda safe to copy and move, because the stored data follows the usual value‑semantic rules. Capturing by reference creates a reference member. The closure can be copied, but the copies still refer to the original variable. This nuance matters when the lambda is stored in a container that outlives the referenced variable. + +### Performance considerations +A capture‑less lambda can be converted to a plain function pointer. The conversion eliminates the indirect call overhead. When a lambda captures state, the compiler usually inlines the call if the closure type is known at compile time. Inlining removes the call indirection entirely. The cost of capturing by copy is the extra copy operation performed at the point of lambda creation. + +## `std::function` and type erasure +`std::function` is a class template that abstracts away the concrete callable type. Internally it stores a pointer to a type‑erased function object and a pointer to a virtual call dispatcher. When a callable is assigned to a `std::function`, the wrapper can allocate dynamic memory if the object does not fit into the Small‑Object Optimization buffer (typically 2–3 pointers). The allocation adds heap traffic and a level of indirection at each invocation. + +The trade‑off is flexibility. A `std::function` can hold any callable that matches the required signature, regardless of its type. This enables runtime polymorphism: a program can fill a container with heterogeneous callables, select one based on user input, and invoke it without knowing its exact type. + +In contrast, a template parameter that accepts a callable preserves the concrete type. The compiler can inline the call, eliminate the virtual dispatcher, and avoid any heap allocation. This zero‑overhead approach is the default recommendation for performance‑critical code. + +The example below builds a `std::vector>`. It stores three different callables: a free function that multiplies its argument by three, a capturing lambda that adds five, and a `std::bind` expression that multiplies by five. The program iterates the vector, calls each element with the argument `5`, and prints the result. + +```cpp +{{#include ../../examples/ch14/function_vector.cpp}} +``` + +The test expects the three lines `15`, `10`, and `25` in that order. The example shows how heterogeneous callables can coexist in a single container. + +### Allocation behaviour +If the stored callable fits into the small‑object buffer, `std::function` does not allocate. In the example the lambda and the bound function are small enough, so no heap allocation occurs. If a callable captures a large `std::vector` by value, the wrapper will allocate to hold the captured data. + +### When to prefer `std::function` +* When the callable type is not known at compile time, such as when a plugin supplies a callback. +* When the callable must be stored in a homogeneous container that outlives the point of creation. +* When the API is a boundary that other languages or runtimes will call. + +## When to erase, when to template +The choice between type erasure and a template hinges on the point at which the callable is selected. + +* If the algorithm is a library component that the user instantiates, the library must expose a template parameter (or a generic `auto` parameter) for the callable. This yields a bespoke instantiation for each caller, allowing the compiler to inline the call and generate optimal code. +* If the algorithm is part of a runtime system, such as a GUI framework that stores user‑supplied callbacks, a networking library that registers event handlers, or a scripting engine that invokes user code, type erasure via `std::function` or `std::move_only_function` is appropriate. + +A practical decision rule: +1. Ask whether the callable can be known at compile time. +2. If yes, write a template parameter. The compiler will emit a separate version for each distinct callable type. +3. If no, use `std::function` (or the move‑only variant) to hide the type. + +The rule helps avoid accidental performance loss in tight loops while still providing the flexibility needed at program boundaries. + +## `std::invoke` and `std::invoke_r` +`std::invoke` is a utility that uniformly calls several kinds of callables. It accepts a callable and a set of arguments, then dispatches to the appropriate call expression. The overload handles: +* Regular function objects and function pointers. +* Pointers to member functions, where the first argument is the object (or a reference, pointer, or smart pointer) on which to invoke the member. +* Pointers to data members, where the result is the member value. + +`std::invoke_r(f, args…)` adds an explicit return‑type conversion, forcing the result to be converted to `R` before returning. + +The following program defines a free function, a struct with a member function and a data member, and then calls each through `std::invoke`. The output demonstrates how the same helper covers all three cases. + +```cpp +{{#include ../../examples/ch14/invoke_example.cpp}} +``` + +The test harness checks for the three lines `free: 42`, `member: 14`, and `data: 99`. Using `std::invoke` simplifies generic code because the same syntax works for all callable categories. + +## Function objects / functors +A function object, commonly called a functor, is a class or struct that defines `operator()`. The type is a regular class type, so it can have explicit constructors, data members, and custom copy or move behaviour. Functors are useful when the callable carries significant state that must be part of its type, for example when the state influences overload resolution or when the type must be named for ADL. + +The example below defines a comparator that orders integers by their absolute distance from a reference value. The comparator stores the reference as a data member and implements a `constexpr` call operator. The program creates a vector, sorts it with `std::ranges::sort` using the functor, and prints the sorted sequence. + +```cpp +{{#include ../../examples/ch14/functor_sort.cpp}} +``` + +The test checks that the program prints `1 2 3 4 5`. The functor demonstrates how a stable type can be passed to algorithms without incurring any type‑erasure cost. + +### When a functor is preferable +* When the callable must be copyable or movable in a predictable way. +* When the functor participates in overload sets that depend on its type. +* When the callable is part of a public API and the type name conveys intent. + +## `std::move_only_function` (C++23) +`std::move_only_function` is introduced in C++23 as the move‑only analogue of `std::function`. It provides type erasure while forbidding copying. This enables storage of callables that are not copyable, such as a lambda that captures a `std::unique_ptr` or a class that holds a mutex. + +The move‑only wrapper eliminates the hidden copy‑construction step that `std::function` performs when a copy of the wrapper is made. When the program never copies the wrapper, the overhead is identical to `std::function` but with the added guarantee that the stored callable cannot be duplicated inadvertently. + +> **Note** - The book registers this feature as a *gap* on compilers that do not yet implement the header. The prose explains the semantics. The example is omitted to keep the build portable. + +## Try this +Build a `std::vector>` containing three operations: the identity function, a lambda that squares its argument, and a lambda that adds one. Invoke each stored callable with the value `3.0` and print the three results on separate lines. + +No solution is provided. The reader must write the code, register it as a `book_example`, and verify that the output contains the three numbers `3`, `9`, and `4`. + +--- diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index b389bad..1eefcbe 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -11,3 +11,4 @@ add_subdirectory(ch10) add_subdirectory(ch11) add_subdirectory(ch12) add_subdirectory(ch13) +add_subdirectory(ch14) diff --git a/examples/ch14/CMakeLists.txt b/examples/ch14/CMakeLists.txt new file mode 100644 index 0000000..39992f7 --- /dev/null +++ b/examples/ch14/CMakeLists.txt @@ -0,0 +1,6 @@ +# CMakeLists for chapter 14 examples + +book_example(lambda_predicate.cpp EXPECT "count = 3") +book_example(function_vector.cpp EXPECT "15\n10\n25") +book_example(invoke_example.cpp EXPECT "free: 42\nmember: 7\ndata: 99") +book_example(functor_sort.cpp EXPECT "1 2 3 4 5") diff --git a/examples/ch14/function_vector.cpp b/examples/ch14/function_vector.cpp new file mode 100644 index 0000000..d777464 --- /dev/null +++ b/examples/ch14/function_vector.cpp @@ -0,0 +1,19 @@ +#include +#include +#include +#include + +int triple(int x) { return x * 3; } + +int main(){ + std::vector> ops; + ops.emplace_back(triple); // free function + ops.emplace_back([](int x){ return x + 5; }); // lambda adds five + using namespace std::placeholders; + ops.emplace_back(std::bind([](int a, int b){ return a * b; }, _1, 5)); // multiply by 5 + + for(const auto &op : ops){ + std::cout << op(5) << std::endl; + } + return 0; +} diff --git a/examples/ch14/functor_sort.cpp b/examples/ch14/functor_sort.cpp new file mode 100644 index 0000000..ae3fa48 --- /dev/null +++ b/examples/ch14/functor_sort.cpp @@ -0,0 +1,20 @@ +#include +#include +#include +#include +#include + +struct DistanceComparator { + int ref; + constexpr bool operator()(int a, int b) const { + return std::abs(a - ref) < std::abs(b - ref); + } +}; + +int main(){ + std::vector v = {5,1,3,2,4}; + std::ranges::sort(v, DistanceComparator{0}); // sort by distance from 0 (i.e., absolute value) + for(int x: v) std::cout << x << ' '; + std::cout << std::endl; + return 0; +} diff --git a/examples/ch14/invoke_example.cpp b/examples/ch14/invoke_example.cpp new file mode 100644 index 0000000..11179a1 --- /dev/null +++ b/examples/ch14/invoke_example.cpp @@ -0,0 +1,26 @@ +#include +#include +#include + +struct Obj { + int value = 42; + int get() const { return value; } + int data = 99; +}; + +int free_func(int x) { return x; } + +int main(){ + // free function via std::invoke + std::cout << "free: " << std::invoke(free_func, 42) << std::endl; + + Obj o{ .value = 7, .data = 99 }; + // pointer to member function + auto mem_fn = &Obj::get; + std::cout << "member: " << std::invoke(mem_fn, o) << std::endl; + + // pointer to data member + auto mem_data = &Obj::data; + std::cout << "data: " << std::invoke(mem_data, o) << std::endl; + return 0; +} diff --git a/examples/ch14/lambda_predicate.cpp b/examples/ch14/lambda_predicate.cpp new file mode 100644 index 0000000..ed293d5 --- /dev/null +++ b/examples/ch14/lambda_predicate.cpp @@ -0,0 +1,12 @@ +#include +#include +#include +#include + +int main(){ + std::vector v = {1,2,3,4,5,6}; + auto is_even = [](int x){ return x % 2 == 0; }; + auto count = std::ranges::count_if(v, is_even); + std::cout << "count = " << count << std::endl; + return 0; +}