C++: the observer pattern with std::function
Notify any number of listeners without the publisher knowing what they do.
#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";
}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
#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";}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
| Step / line | What happens | State after step | Output 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.