C++: the observer pattern with std::function

Notify any number of listeners without the publisher knowing what they do.

C++
#include <functional>
#include <iostream>
#include <string>
#include <vector>

class EventBus {
 public:
  void subscribe(std::function<void(const std::string&)> listener) {
    listeners_.push_back(std::move(listener));
  }

  void publish(const std::string& event) const {
    for (const auto& listener : listeners_) {
      listener(event);
    }
  }

 private:
  std::vector<std::function<void(const std::string&)>> listeners_;
};

int main() {
  EventBus bus;
  std::vector<std::string> log;

  bus.subscribe([&log](const std::string& e) { log.push_back(e); });
  bus.subscribe([](const std::string& e) { std::cout << "got " << e << "\n"; });

  bus.publish("order_placed");
  bus.publish("order_shipped");
  std::cout << "logged " << log.size() << " events\n";
}
Output
got order_placed
got order_shipped
logged 2 events

Explanation

The observer pattern separates the code that produces an event from the code that reacts to it. The publisher keeps a list of callbacks and calls each one when something happens. New behavior, such as logging or an email, is added by subscribing another callback, with no change to the publisher.

std::function can hold any callable with a matching signature: a lambda, a function pointer, or a function object. Capturing by reference lets a listener update outside state, but then the referenced object must outlive the subscription, or the callback will use a dangling reference.

Explore the dry run
Guided walkthroughReady
1#include <functional>
2#include <iostream>
3#include <string>
4#include <vector>
5
6class EventBus {
7 public:
8 void subscribe(std::function<void(const std::string&)> listener) {
9 listeners_.push_back(std::move(listener));
10 }
11
12 void publish(const std::string& event) const {
13 for (const auto& listener : listeners_) {
14 listener(event);
15 }
16 }
17
18 private:
19 std::vector<std::function<void(const std::string&)>> listeners_;
20};
21
22int main() {
23 EventBus bus;
24 std::vector<std::string> log;
25
26 bus.subscribe([&log](const std::string& e) { log.push_back(e); });
27 bus.subscribe([](const std::string& e) { std::cout << "got " << e << "\n"; });
28
29 bus.publish("order_placed");
30 bus.publish("order_shipped");
31 std::cout << "logged " << log.size() << " events\n";
32}

Follow the example

Start to inspect the source, values, and output. These prepared steps explain the published sample; grouped loop steps describe multiple iterations.

View the complete dry-run table
Prepared trace of the published program. Some rows group repeated iterations.
Step / lineWhat happensState after stepOutput so far
Any callable taking a string can be registered.—
—
This listener records events in a vector captured by reference.—
—
Both listeners run in registration order; only the second prints.—
got order_placed
The recording listener saw both events.log = [order_placed, order_shipped]
got order_placed
got order_shipped
logged 2 events

Cost and common mistakes

publish is O(L) for L listeners.

  • A listener that captures a local variable by reference becomes dangerous once that variable is destroyed.
  • If a listener subscribes or unsubscribes during publish, modifying the vector under the loop invalidates iterators.

Try it yourself

How do you let a listener be removed later?

Reveal answer and reasoning

Return an id from subscribe and keep listeners in a map from id to callback, then erase by id.

Further reading