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::function signatures.

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