Practice Lab 28 — Notification framework from containers and callables¶
Difficulty: Advanced · Estimated time: 6 hours · Module 14: STL with OOP
A framework made of an interface, a container, a function object and a lifetime rule. Every piece is standard-library; the design is yours.
At a glance¶
| Difficulty | Advanced |
| Estimated time | 6 hours |
| Module | Module 14 — The STL with OOP |
| Concepts practised | Storing polymorphic objects without slicing, function objects, std::function and lambdas, iteration over a container of callables, ordering, lifetime of subscribers |
| Prerequisite | Practice Lab 27 |
| Class design | yours — the requirements and the lifetime rule are given |
Scenario¶
A monitoring service must tell interested parts of the system when something happens: a device goes offline, a threshold is crossed, a batch finishes. The subscribers are of four kinds, and they differ in how they are invoked:
- an object with a method (the audit log),
- a free function (the existing alarm routine, which cannot be changed),
- a lambda capturing local state (an ad-hoc counter in
main), - a plain callable object with its own state (a rate limiter that must count calls).
The subtle requirement is the one that breaks naive implementations: a subscriber must not be notified after it has been destroyed, and unsubscribing must be possible during a notification.
Problem statement¶
Build a notification framework.
- A notification record: an event kind, a source, a severity, a timestamp or counter, and a payload that can be a number or text.
- A subscriber interface — and the design decision recorded: is a subscriber an abstract class, a
std::function, or both, with an adapter for the other? Argue it. - A registry that accepts all four subscriber kinds above, and can unsubscribe one.
- Ordered delivery within a severity or priority, so two subscribers registered at the same priority are notified in a documented order.
- Safe unsubscription during notification — demonstrated: a subscriber that unsubscribes itself from inside its own notification, and one that unsubscribes a different subscriber mid-notification. Neither may skip or double-notify anybody.
- No notification after destruction — a subscriber object destroyed while registered must not be called. Demonstrate what happens with your design, and record the rule that makes it safe (a destructor that unsubscribes, a token, or a documented ownership requirement).
- Filtering: a subscriber may subscribe only to certain event kinds or severities, expressed as a
predicate rather than an
ifinside the subscriber. - A bulk dispatch: a batch of notifications processed with a standard algorithm, and the summary produced with another.
In framework-design.md:
- The subscriber representation you chose and the alternative's cost, in one paragraph.
- The container holding subscribers, its ordering rule, and why that container.
- The lifetime rule, stated as something callers must do — and what the framework does if they do not.
- The mutate-during-iteration problem: what the standard containers do to a loop that unsubscribes, and how your dispatch avoids it.
- The filtering predicate's type and where it lives.
Learning objectives¶
By the end of this lab you will be able to:
- Store and invoke polymorphic behaviour from four different kinds of callable behind one interface.
- Choose between an abstract subscriber and a
std::functionand defend the choice. - Iterate a container that is being modified, safely and deliberately.
- Enforce and document a lifetime rule for subscribers.
- Express filtering as a predicate instead of a branch inside the subscriber.
Functional requirements¶
- All four subscriber kinds work without modifying the free function.
- The audit log records every notification it receives, in the order received.
- The rate limiter's own state changes across calls and is observable.
- A subscriber added later does not receive earlier notifications; one removed earlier receives none after.
- Self-unsubscription during a notification neither skips the next subscriber nor double-notifies.
- Unsubscribing another subscriber mid-notification has the same guarantee, and the behaviour is documented for the already-notified and not-yet-notified cases.
- A destroyed subscriber that did not unsubscribe is not called — through your documented rule, not luck.
- Filtering happens before dispatch, and a filtered-out subscriber does no work.
- Dispatch of 1 000 notifications prints a summary: count by kind, count by severity, and per subscriber.
- No dangling subscriber is ever invoked; a test or instrumented count proves it.
Constraints¶
- Header and source per type family, with a documented build command.
- The free function must be adapted, not edited — record the adapter.
- Explicit reference counting of dispatch calls, so "notified twice" is detectable.
- No warnings.
Starter task¶
In framework-design.md, before coding:
- Decide the subscriber representation and write the alternative's cost in one paragraph.
- Write the lifetime rule as an instruction to callers, and one enforcement mechanism you could add.
- Predict what happens when a subscriber unsubscribes itself during iteration in each of: a vector, a list, an ordered container. Then check the predictions.
- Write the filter predicate's type and one example usage.
- Write the ordering rule for same-priority subscribers, and where it is enforced.
Implementation tasks¶
- Implement the notification record and severity.
- Implement the subscriber representation and the registry with its ordering.
- Adapt the four subscriber kinds, including the free function.
- Implement dispatch with safe mutation — via a copy, a two-phase mark-and-sweep, or a documented alternative — and justify the choice.
- Implement filtering as a predicate.
- Write the lifetime demonstration: destroy a subscriber without unsubscribing and show the outcome.
- Run the 1 000-notification batch and print the summary.
Test cases¶
| # | Action | Expected result |
|---|---|---|
| 1 | Notify with all four kinds registered | All four called, in the documented order |
| 2 | A subscriber added after a notification | Receives only later ones |
| 3 | Unsubscribe, then notify | Not called |
| 4 | A subscriber unsubscribes itself | No skip, no double notification |
| 5 | A subscriber unsubscribes another, not yet notified | That one is not notified (documented behaviour) |
| 6 | A subscriber unsubscribes another, already notified | No effect on this round |
| 7 | Destroy a registered subscriber without unsubscribing | Not called; the rule's outcome recorded |
| 8 | Two subscribers at the same priority | Documented, repeatable order |
| 9 | Filter on one event kind | Only matching notifications reach it |
| 10 | 1 000 notifications | Summary: by kind, by severity, per subscriber |
| 11 | Count every invocation | No subscriber counted twice in one round |
| 12 | Rate limiter after 100 calls | Its own state reflects all calls |
Boundary cases to run deliberately: no subscribers; one subscriber; unsubscribing an unregistered subscriber; a filter that matches nothing; a notification with an empty payload.
Expected behaviour¶
registered: audit(log), alarm(free), counter(lambda), limiter(object)
notify(device_offline, high) ->
audit: [high] device_offline src=PLC-3
alarm: ALARM high device_offline
counter: 1
limiter: 1 of 5 allowed
self-unsubscribe during notification:
audit unsubscribed itself; the remaining three were still called exactly once
another subscriber unsubscribed mid-round:
limiter removed the counter; counter was already notified this round -> still once
destroyed without unsubscribing:
audit's object went out of scope; dispatch called 3 subscribers (audit skipped by its rule)
1000 notifications: device_offline 400, threshold 350, batch_done 250
per subscriber: audit 1000, alarm 1000, counter 1000, limiter 5 (rate-limited)
Extension challenge¶
Make delivery asynchronous: notifications are queued and drained later, in one place.
Record in framework-design.md:
- What changes about the lifetime rule once delivery is deferred — and why it becomes stricter.
- How the queue interacts with filtering: at enqueue or at drain, and what each costs.
- What happens to a notification queued for a subscriber that unsubscribes before the drain.
- The change cost: files, operations, lines, and one new failure mode.
Debugging challenge¶
A subscriber registered twice is notified twice in one round, and one subscriber is skipped entirely when another unsubscribes:
void Notifier::dispatch(const Notification& n) {
for (auto& sub : subscribers_) { // vector<unique_ptr<Subscriber>>
if (sub->interested(n)) {
sub->notify(n);
if (sub->done()) {
subscribers_.erase(find_subscriber(sub.get())); // during the loop
}
}
}
}
Reproduce it — register a self-unsubscribing subscriber before another one — and answer:
- What does erasing during the loop do to the current and next elements, in this container?
- Which iterator becomes invalid, and which element is skipped as a result?
- Why does the same bug not show with a hash container, and what new problem appears there instead?
- Which of the twelve test cases would catch it, and what would the counts show?
Reflection questions¶
- Did you choose an abstract subscriber or a callable, and what did the other cost? Would you choose the same again for a framework with 50 subscriber types?
- Which subscriber kind was hardest to adapt, and why?
- The lifetime rule is a requirement on callers. What is the difference between a rule you document and one the framework cannot break? Which did you implement?
- Unsubscribing mid-round: which of the two possible behaviours (this round or next round) is right, and who should decide?
- Filtering with a predicate instead of a branch inside the subscriber — what did that buy, and what does it cost at registration?
- The rate limiter's state changed across calls. Who owns that state, and who is responsible for resetting it?
Completion checklist¶
- [ ] It builds from a documented command with
-Wall -Wextra -Werrorand no warnings - [ ] All four subscriber kinds work, with the free function adapted rather than edited
- [ ]
framework-design.mdrecords the representation choice and the lifetime rule - [ ] Self-unsubscription during dispatch is safe — no skip, no double notification
- [ ] Unsubscribing another subscriber mid-round behaves as documented
- [ ] A destroyed subscriber is never invoked, through the documented rule
- [ ] Filtering is a predicate, and a filtered-out subscriber does no work
- [ ] The 1 000-notification summary is printed
- [ ] Invocation counts prove no double notification
- [ ] All twelve test cases run, with output recorded
- [ ] The debugging challenge was explained before being fixed
- [ ] I can state the ordering rule and the container that provides it
Related: Lab index · Module 14 — The STL with OOP · Previous: Practice Lab 27 · Next: Practice Lab 29 — A plugin registry behind one interface