The observer pattern
An observer is a subscriber that wants to know when something changes. The subject keeps a list of them and calls each one after the change, so the code that changes the data does not know who cares about it.
The pattern is the base of every event system: a document with views, a model with widgets, a cache with invalidators. This lesson builds it three ways, from a class interface to a list of callbacks and a list of weak references, and meets the observer that never implemented its method.
A notification that throws is the failure mode to decide about: the observers after the throw are not called and the subject does not know. A catch around each callback keeps the others alive, at the cost of a decision about where the error goes.
A subject with many observers is also a place where the notification order matters, and the interface does not promise one. An observer that depends on another being called first is a design that should be one observer instead.
An interface and a list
The subject stores std::vector<observer*> and calls update(value) on each after a change. The observers are interfaces, so the subject needs no template and no knowledge of what the observer does.
The program attaches two observers to a counter, changes it twice and prints what each one saw. The first observer keeps the sum, the second the count, and both print after the notifications.
The raw pointers mean the subject does not own the observers, which is right: an observer may outlive the subject or be destroyed first. The last case is the danger the weak version later solves, because a stale pointer in the vector is a use-after-free.
#include <iostream>
#include <vector>
class observer
{
public:
virtual ~observer() = default;
virtual void update(int value) = 0;
};
class subject
{
public:
void attach(observer* watcher)
{
watchers_.push_back(watcher);
}
void set(int value)
{
value_ = value;
for (observer* watcher : watchers_) {
watcher->update(value_);
}
}
private:
std::vector<observer*> watchers_;
int value_ = 0;
};
class sum_view : public observer
{
public:
void update(int value) override { total_ += value; }
int total() const { return total_; }
private:
int total_ = 0;
};
class count_view : public observer
{
public:
void update(int) override { ++calls_; }
int calls() const { return calls_; }
private:
int calls_ = 0;
};
int main()
{
subject source;
sum_view sum;
count_view count;
source.attach(&sum);
source.attach(&count);
source.set(10);
source.set(20);
std::cout << sum.total() << ' ' << count.calls() << '\n';
}
g++ -std=c++23 -Wall -Wextra interface.cpp -o observer_interface
./observer_interface
30 2
Callbacks instead of a class
A std::vector<std::function<void(int)>> accepts lambdas that capture whatever the subscriber needs. There is no interface to implement, and the subscriber can be a local lambda with one line.
The program attaches a lambda that collects the values and a function that counts them. The output is 30 and 2, and neither subscriber is a class.
The cost is the type-erased call of lesson 125 and the requirement that the callable is copyable. The benefit is that a subscriber with state is a lambda with a capture, which is less code than a class with one virtual method.
#include <functional>
#include <iostream>
#include <vector>
class subject
{
public:
void subscribe(std::function<void(int)> callback)
{
callbacks_.push_back(std::move(callback));
}
void set(int value)
{
for (const auto& callback : callbacks_) {
callback(value);
}
}
private:
std::vector<std::function<void(int)>> callbacks_;
};
int main()
{
subject source;
int total = 0;
int calls = 0;
source.subscribe([&total](int value) { total += value; });
source.subscribe([&calls](int) { ++calls; });
source.set(10);
source.set(20);
std::cout << total << ' ' << calls << '\n';
}
g++ -std=c++23 -Wall -Wextra callbacks.cpp -o observer_callbacks
./observer_callbacks
30 2
A weak reference to the subscriber
A list of std::weak_ptr<observer> does not keep an observer alive. Before each notification the subject locks the weak pointer; an observer that was destroyed fails the lock and is dropped from the list.
The program attaches two subscribers, destroys one, and notifies. Only the survivor receives the update, and the print is 1 for the number of live watchers and 10 for the value it saw.
The pruning loop is the part that matters: an expired entry is erased, so the list does not grow with dead subscribers. Lesson 092 showed the weak pointer itself; here it is the ownership rule of a subscription.
#include <iostream>
#include <memory>
#include <vector>
class observer
{
public:
virtual ~observer() = default;
virtual void update(int value) = 0;
};
class subject
{
public:
void attach(std::weak_ptr<observer> watcher)
{
watchers_.push_back(std::move(watcher));
}
void set(int value)
{
for (auto it = watchers_.begin(); it != watchers_.end();) {
if (auto watcher = it->lock()) {
watcher->update(value);
++it;
} else {
it = watchers_.erase(it);
}
}
}
std::size_t size() const { return watchers_.size(); }
private:
std::vector<std::weak_ptr<observer>> watchers_;
};
class keeper : public observer
{
public:
void update(int value) override
{
value_ = value;
++calls_;
}
int value() const { return value_; }
int calls() const { return calls_; }
private:
int value_ = 0;
int calls_ = 0;
};
int main()
{
subject source;
auto first = std::make_shared<keeper>();
auto second = std::make_shared<keeper>();
source.attach(first);
source.attach(second);
second.reset();
source.set(10);
std::cout << source.size() << ' ' << first->value() << ' ' << first->calls() << '\n';
}
g++ -std=c++23 -Wall -Wextra weak.cpp -o observer_weak
./observer_weak
1 10 1
The abstract observer
An observer that declares update but does not override it stays abstract, and a variable of that type does not compile. The message says the class is abstract because of the pure virtual function it still has.
The program's logger derives from observer and forgets the override. The compiler names the class and the function that keeps it abstract, and it points at the declaration in main.
The fix is the override, and override on the function is what makes a signature mismatch a compile error at the class instead of a silently abstract type. A forgotten parameter is the usual cause, and the keyword catches it.
class observer
{
public:
virtual ~observer() = default;
virtual void update(int value) = 0;
};
class logger : public observer
{
public:
void update() {}
};
int main()
{
logger instance;
}
g++ -std=c++23 -Wall -Wextra abstract.cpp -o abstract
abstract.cpp:5:18: warning: 'virtual void observer::update(int)' was hidden [-Woverloaded-virtual=]
5 | virtual void update(int value) = 0;
| ^~~~~~
abstract.cpp:11:10: note: by 'void logger::update()'
11 | void update() {}
| ^~~~~~
abstract.cpp: In function 'int main()':
abstract.cpp:16:12: error: cannot declare variable 'instance' to be of abstract type 'logger'
16 | logger instance;
| ^~~~~~~~
abstract.cpp:8:7: note: because the following virtual functions are pure within 'logger':
8 | class logger : public observer
| ^~~~~~
abstract.cpp:5:18: note: 'virtual void observer::update(int)'
5 | virtual void update(int value) = 0;
| ^~~~~~
Exercises
- Add a detach method to the interface version that removes an observer by address.
- Make the callback list return an id from subscribe and add an unsubscribe by id.
- Give the weak-pointer subject a method that returns the number of live watchers after a notification.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
What does the subject do when its value changes?
Answer
It calls every attached observer
The notification loop is the whole pattern: the subject pushes the change to its subscribers.
-
Why does the subject store pointers rather than observer objects?
Answer
Because an observer may be destroyed before the subject
The subject does not own its observers, and a pointer leaves the lifetime to the owner of the observer.
-
What does a
std::weak_ptrsubscription guarantee?Answer
A destroyed observer is skipped and removed from the list
The lock fails for an expired observer, and the subject erases the entry.
-
What does this program print?
#include <iostream> #include <vector> struct observer { virtual ~observer() = default; virtual void update(int) = 0; }; struct subject { std::vector<observer*> watchers; void attach(observer* o) { watchers.push_back(o); } void set(int value) { for (observer* o : watchers) o->update(value); } }; struct sum_view : observer { int total = 0; void update(int value) override { total += value; } }; struct count_view : observer { int calls = 0; void update(int) override { ++calls; } }; int main() { subject s; sum_view sum; count_view count; s.attach(&sum); s.attach(&count); s.set(10); s.set(20); std::cout << sum.total << ' ' << count.calls; }Answer
30 2
The sum view adds 10 and 20, and the count view saw two notifications.
-
What does this program print?
#include <functional> #include <iostream> #include <vector> int main() { std::vector<std::function<void(int)>> callbacks; int total = 0, calls = 0; callbacks.push_back([&total](int value) { total += value; }); callbacks.push_back([&calls](int) { ++calls; }); for (int value : {10, 20}) for (const auto& callback : callbacks) callback(value); std::cout << total << ' ' << calls; }Answer
30 2
The lambdas capture the two locals, and the same two notifications run.
-
What does this program print?
#include <iostream> #include <memory> #include <vector> struct observer { virtual ~observer() = default; virtual void update(int) = 0; }; struct subject { std::vector<std::weak_ptr<observer>> watchers; void attach(std::weak_ptr<observer> w) { watchers.push_back(w); } void set(int value) { for (auto it = watchers.begin(); it != watchers.end();) { if (auto w = it->lock()) { w->update(value); ++it; } else it = watchers.erase(it); } } std::size_t size() const { return watchers.size(); } }; struct keeper : observer { int value = 0; int calls = 0; void update(int v) override { value = v; ++calls; } }; int main() { subject s; auto first = std::make_shared<keeper>(); auto second = std::make_shared<keeper>(); s.attach(first); s.attach(second); second.reset(); s.set(10); std::cout << s.size() << ' ' << first->value << ' ' << first->calls; }Answer
1 10 1
The dead observer is erased, so the size is 1, and the survivor saw 10 once.
-
When is the expired observer removed from the list?
Answer
During the next notification, when its weak pointer fails to lock
The subject has no way to know about the destruction, so the pruning happens at the next walk.
-
The build stops at this declaration. What does the compiler report?
struct observer { virtual void update(int) = 0; }; struct logger : observer { void update() {} }; logger instance;Answer
logger is abstract because update(int) is not overridden
The pure virtual function is still there, so no object of the derived type can be created.
-
Why is
overrideworth writing on an observer method?Answer
A signature mismatch becomes an error at the class
Without it a wrong signature is a new function, and the class silently stays abstract.
-
Which subscription can capture local state?
Answer
A lambda in a std::function list
The capture is stored in the function object, which the interface and the pointer cannot do.
The code on this page was built with g++ 15.2.0 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container. The programs whose section shows a compiler error were compiled until that error and were not run.
Article author: Arthur Isaev