DMarlon Studio

Lesson 18 · 18 of 20

std::function and callbacks · C++

Build an event system in C++ with std::function: register lambdas, functions and methods as callbacks and call them when each event fires.

C++ · std::function and callbacks in DMarlon Studio: the example's code and its output in the terminal

A callback is a function you hand to another part of the program so it can call it when something happens. That is how buttons in a user interface work: in the FLTK desktop apps that DMarlon Studio generates, each click calls the function attached to it.

`std::function<void(const std::string&)>` can hold anything that can be called with that signature: a lambda, a plain function or an object with operator(). using Handler = ... gives it a name, and handlers_ maps each event name to its list of callbacks.

on() registers a callback for an event and emit() calls all of that event's callbacks, in order. click has two: a lambda that counts clicks by capturing clicks by reference, and the free function writeLog, which fits because it has the same signature.

To call a method on an object, use a lambda that captures it: [&cart](const std::string& p) { cart.add(p); }. The cart adds one item on each add. When close is emitted, there are no callbacks for it, so the loop finds nothing and nothing else happens.

Watch out for captures by reference: the bus stores the lambdas, so cart and clicks must stay alive for as long as events can be emitted. Here everything lives in main, so all is well.

Code · main.cpp

#include <functional>#include <iostream>#include <map>#include <string>#include <vector>// A small event system: each event name keeps a list of callbacks.class EventBus {    using Handler = std::function<void(const std::string&)>;    // any callable with this signature    std::map<std::string, std::vector<Handler>> handlers_;public:    void on(const std::string& event, Handler h) { handlers_[event].push_back(h); }    void emit(const std::string& event, const std::string& data) {        std::cout << "emit " << event << "\n";        for (const Handler& h : handlers_[event]) h(data);    }};void writeLog(const std::string& data) { std::cout << "  [log] " << data << "\n"; }struct Cart {    int items = 0;    void add(const std::string& p) { std::cout << "  [cart] " << p << ", " << ++items << " item(s)\n"; }};int main() {    EventBus bus; Cart cart; int clicks = 0;    bus.on("click", [&clicks](const std::string& button) { std::cout << "  [ui] click " << ++clicks << ": " << button << "\n"; });    bus.on("click", writeLog);                                  // a plain function fits too    bus.on("add", [&cart](const std::string& p) { cart.add(p); });   // calls a method    bus.emit("click", "Buy now");    bus.emit("add", "Headphones");    bus.emit("add", "Charger");    bus.emit("close", "nobody listens");                        // no handlers: nothing runs}

Output · c++ -std=c++17 main.cpp -o main && ./main

emit click
  [ui] click 1: Buy now
  [log] Buy now
emit add
  [cart] Headphones, 1 item(s)
emit add
  [cart] Charger, 2 item(s)
emit close

Key points

  • A callback is a function that some other code will call later.
  • std::function<R(Args...)> holds any callable with that signature.
  • Lambdas, free functions and objects with operator() all fit.
  • For a method, capture the object in a lambda.
  • Anything captured by reference must outlive the callback.

Exercise

Add an off(event) method that removes every callback for an event. Call bus.off("click") and emit another click to check that nothing is printed any more.