<< Home

Chapter 26: Functional Patterns and Callbacks

C++ supports a spectrum of functional styles. You can pass functions as values, compose work through lightweight views, and build callback based APIs that stay predictable and efficient. This chapter explores higher order helpers, std::function compared with templates, composable view pipelines, and callback design that avoids hidden traps.

Higher order utilities and bind

Higher order utilities treat functions as data. You can partially apply a function, adapt signatures, or connect algorithms with small glue objects. In modern code, lambdas usually replace std::bind, but understanding both techniques helps when maintaining older code or integrating with generic interfaces.

Partial application with lambdas

Lambdas capture values and create function objects. This gives you clean partial application without extra machinery. Use captures to bind arguments or context that should travel with the operation.

#include <algorithm>
auto is_shorter_than = [](std::size_t n) {
  return [n](std::string_view s) { return s.size() < n; };
};
std::vector<std::string> v{"alpha", "beta", "gamma"};
v.erase(std::remove_if(v.begin(), v.end(), is_shorter_than(5)), v.end());

Adapting signatures with std::bind

std::bind reorders arguments and fixes parameters to new values. Modern C++ prefers lambdas for clarity, but existing libraries still expose bind friendly hooks. Use placeholders to pick which arguments get forwarded.

#include <functional>
using std::placeholders::_1;
int add(int a, int b) { return a + b; }
auto add_to_10 = std::bind(add, 10, _1);
int r = add_to_10(7);  // r is 17
πŸ’‘ Keep captures explicit. A lambda that lists its captures in plain sight is easier to audit than a bind expression with several placeholders.

Function adaptors in algorithms

Standard algorithms often accept predicates, projections, or mappers. Writing tiny adaptors keeps pipelines readable and minimizes boilerplate. Use captures to lift constants or shared state into the algorithm.

#include <algorithm>
#include <numeric>
std::vector<int> v{1, 2, 3, 4, 5};
int sum_sq = std::accumulate(v.begin(), v.end(), 0,
  [](int acc, int x) { return acc + x * x; });

std::function vs templates

Passing callables can be dynamic or static. std::function provides type erased wrappers for runtime flexibility, while templates give compile time customization without dynamic overhead. Pick the model that matches your cost and flexibility requirements.

Dynamic dispatch with std::function

std::function stores any callable of a compatible signature. This makes it ideal for plugin points, event systems, and user provided callbacks whose concrete type you cannot know ahead of time.

#include <functional>
std::function<int(int)> f;
f = [](int x) { return x + 1; };
f = [](int x) { return x * 2; };
int r = f(10);

Static dispatch with templates

When performance is critical and the callable type is known at compile time, take the callable as a template parameter. This avoids type erasure and inline barriers, letting the compiler optimize aggressively.

template <class F>
int apply_twice(F f, int x) {
  return f(f(x));
}
int r = apply_twice([](int x){ return x + 1; }, 10);
⚠️ Do not store templated callables directly in containers when types vary per element. Either wrap them in std::function or use variant based storage.

Small buffer optimization effects

std::function often includes small buffer optimization. Many tiny callables fit without heap allocation. Large captures may spill to the heap, so audit captures to keep performance stable.

#include <functional>
int big_array[1024];
std::function<int()> f = [a = big_array]() { return a[0]; };  // likely allocates

View pipelines and composability

Ranges and views give you lazy pipelines. They transform, filter, and group data without building intermediate containers. Each view adapts the previous stage, so the final algorithm pulls only the elements it needs.

Transform and filter

A transform view maps each element to a new value. A filter view keeps only the values that satisfy a predicate. Chain them to express intent without loops or temporary vectors.

#include <ranges>
std::vector<int> v{1, 2, 3, 4, 5, 6};
auto pipeline =
  v
  | std::views::filter([](int x){ return x % 2 == 0; })
  | std::views::transform([](int x){ return x * x; });
for (int x : pipeline) {
  // … use x
}

Chaining with | for clarity

The pipe operator keeps the transformation flow legible. Each step states one intent, then yields to the next. Use descriptive lambdas to guide readers along the path.

auto words = std::views::transform([](int x){ return std::to_string(x); });
auto r =
  v
  | std::views::filter([](int x){ return x > 10; })
  | words;

Custom view adaptors

You can define your own adaptors to slot into pipelines. These combine a factory function and a view type that wraps an underlying range. Follow existing view patterns to integrate with std::views naturally.

#include <ranges>
template <std::ranges::input_range R>
class take_even_view : public std::ranges::view_interface<take_even_view<R>> {
  R base_;
public:
  take_even_view(R base) : base_(std::move(base)) {}
  auto begin() { return std::ranges::begin(base_); }
  auto end()   { return std::ranges::end(base_); }
  // … implement skipping logic
};
πŸ’‘ Keep custom views simple. Favor composition of existing views over building new iterator types unless you need custom traversal or caching behavior.

Designing callback based APIs

Callbacks notify callers at specific moments such as completion, progress, or error events. A clean design avoids hidden state, captures execution context clearly, and provides strong lifetime rules for every callable you accept.

Style and lifetime of callbacks

Require that callbacks be valid for the duration of their registration. Choose either copy based storage with std::function or move only callables in templates. Document when callbacks are invoked relative to the triggering function.

class downloader {
  std::function<void(double)> on_progress_;
public:
  void set_progress_handler(std::function<void(double)> cb) {
    on_progress_ = std::move(cb);
  }
  void run() {
    for (int i = 0; i <= 100; i += 10) {
      if (on_progress_) on_progress_(i / 100.0);
      // … work
    }
  }
};

Execution context

Explain where callbacks run: same thread, worker threads, or a dispatcher. Misplaced assumptions about threading often cause subtle bugs. Use executors or queues to control context explicitly.

template <class Exec, class F>
void post(Exec& ex, F f) {
  ex.enqueue(std::move(f)); // executor decides where f runs
}

Error reporting and cancellation

Callbacks can signal failures or requests to stop. Use separate callbacks for success and error, or use a result type that carries either value or failure. Keep cancellation cooperative so callers remain in control.

#include <variant>
using result_t = std::variant<std::string, std::error_code>;
std::function<void(result_t)> on_done;

Balancing templates and std::function

For library code, offer both static and dynamic paths. Templates give maximum speed when callers can commit to concrete types. std::function provides flexibility for stored callbacks that may change over time. Matching the callback model to usage patterns keeps APIs predictable and approachable.