Instead of making every observer inherit from an abstract base class, you can store callables in std::function: functions, lambdas, or bound member functions. Inheritance goes away, and a one-line callback takes one line to connect.
Key advantage over the traditional approach
With the interface-based version, observers inherit from Observer and override update().
class MyObserver : public Observer { /* ... */ };
With the function-based version, any callable with the right signature can be connected.
subject.subscribe([](int value) { std::cout << value; });
subject.subscribe(&some_function);
subject.subscribe(&object, &Class::member_function);
Structure
Subject {
std::vector<std::function<void(int)>> callbacks;
// Connect any callable matching the signature
void subscribe(std::function<void(int)> callback)
// Call all subscribers
void notify(int value)
}
The code
#include <iostream>
#include <vector>
#include <functional>
#include <algorithm>
// ── Subject (Publisher) ────────────────────────────────────
template<typename EventType>
class Subject {
private:
std::vector<std::function<void(EventType)>> callbacks_;
public:
// Subscribe with a callable (function, lambda, functor)
void subscribe(std::function<void(EventType)> callback) {
callbacks_.push_back(callback);
}
// Unsubscribe by comparing function pointers (works for simple cases)
// For lambdas and complex callables, store handles instead (see below)
void notify(EventType event) {
for (auto& callback : callbacks_) {
callback(event);
}
}
size_t observer_count() const { return callbacks_.size(); }
};
// ── Standalone callback functions ──────────────────────────
void display_callback(int value) {
std::cout << "[Display] Value: " << value << "\n";
}
void logging_callback(int value) {
std::cout << "[Logger] Value: " << value << "\n";
}
// ── Observer class with member callback ────────────────────
class AlertSystem {
private:
int threshold_;
public:
explicit AlertSystem(int threshold) : threshold_(threshold) {}
void on_value_changed(int value) {
if (value > threshold_) {
std::cout << "[Alert] WARNING: " << value
<< " exceeds " << threshold_ << "\n";
}
}
};
// ── Usage with different callable types ────────────────────
int main() {
Subject<int> subject;
// 1. Free function
subject.subscribe(display_callback);
// 2. Another free function
subject.subscribe(logging_callback);
// 3. Member function (using std::bind)
AlertSystem alert(50);
subject.subscribe(std::bind(&AlertSystem::on_value_changed, &alert, std::placeholders::_1));
// 4. Lambda (inline, no separate function needed)
subject.subscribe([](int value) {
std::cout << "[Custom] Lambda observer: " << value << "\n";
});
std::cout << "Connected " << subject.observer_count() << " observers\n\n";
std::cout << "Notifying with value 10:\n";
subject.notify(10);
std::cout << "\nNotifying with value 60:\n";
subject.notify(60);
return 0;
}
Output:
Connected 4 observers
Notifying with value 10:
[Display] Value: 10
[Logger] Value: 10
[Custom] Lambda observer: 10
Notifying with value 60:
[Display] Value: 60
[Logger] Value: 60
[Alert] WARNING: 60 exceeds 50
[Custom] Lambda observer: 60
Managing subscriptions with handles
Unsubscribing is the weak spot in the code above. You cannot easily remove a lambda because it has no name. Return a subscription handle instead:
template<typename EventType>
class Subject {
public:
using Callback = std::function<void(EventType)>;
class SubscriptionHandle {
private:
std::vector<Callback>* callbacks_;
size_t index_;
friend class Subject;
SubscriptionHandle(std::vector<Callback>* callbacks, size_t index)
: callbacks_(callbacks), index_(index) {}
public:
void unsubscribe() {
if (callbacks_ && index_ < callbacks_->size()) {
callbacks_->erase(callbacks_->begin() + index_);
}
}
};
SubscriptionHandle subscribe(Callback callback) {
callbacks_.push_back(callback);
return SubscriptionHandle(&callbacks_, callbacks_.size() - 1);
}
void notify(EventType event) {
for (auto& callback : callbacks_) {
callback(event);
}
}
private:
std::vector<Callback> callbacks_;
};
// ── Usage ──────────────────────────────────────────────────
int main() {
Subject<int> subject;
auto handle = subject.subscribe([](int v) {
std::cout << "Lambda: " << v << "\n";
});
subject.notify(10); // Lambda called
handle.unsubscribe(); // Disconnect
subject.notify(20); // Lambda NOT called
}
Using type erasure for multiple event types
The traditional approach limits each observer to one update signature. With std::function, you need separate subscriptions for each event type:
class EventSystem {
public:
std::vector<std::function<void(int)>> on_int_event;
std::vector<std::function<void(std::string)>> on_string_event;
std::vector<std::function<void()>> on_simple_event;
void subscribe_int(std::function<void(int)> cb) {
on_int_event.push_back(cb);
}
void subscribe_string(std::function<void(std::string)> cb) {
on_string_event.push_back(cb);
}
// ... etc
};
This works but gets verbose. For a cleaner way to handle several event types with type safety, see Object-oriented event system.
Advantages
- Observers don't need a common base class.
- Lambdas, function pointers, member functions, and functors all work.
- You write less boilerplate than with virtual functions.
- Tests can pass lambdas directly instead of defining mock classes.
- Handlers can be defined inline where they are used.
Disadvantages
- Every callable becomes a
std::function, which erases its compile-time type. - Calls go through a function pointer, and small-object optimization can spill to the heap.
- Anonymous lambdas are hard to remove without a handle.
- Unlike class-based observers, lambdas only reach data they capture.
- Notification order is unspecified, as in the traditional pattern.
When to use
- You have many simple event sources (buttons, sliders) with lightweight handlers.
- Handlers are one-liners or short lambdas.
- You would rather not create observer classes for trivial callbacks.
- You need to integrate with event systems that expect
std::functionsignatures.
Comparison with other approaches
| Aspect | Interface-Based | Function-Based | Qt Signals/Slots | Event System |
|---|---|---|---|---|
| Inheritance needed | Yes | No | No (macros) | Sometimes |
| Type safety | Partial | Lost (type erased) | Full (compile-time) | Full |
| Lambdas | Awkward | Natural | Natural | Natural |
| Unsubscribe | Pointer comparison | Needs handles | Disconnect method | Needs handles |
| Performance | Fast (direct vtable) | Slower (type erasure) | Very fast (compile-time) | Fast (virtual) |
| Framework | Standard C++ | Standard C++ | Qt-specific | Standard C++ |
Next steps
- Qt signals and slots provides a type-safe signal/slot mechanism checked at compile time.
- Object-oriented event system adds extensible, polymorphic event objects for more complex cases.
- Lifetime management covers observer and callback lifetime with smart pointers.