GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 93.3% 152 / 0 / 163
Functions: 100.0% 32 / 0 / 32
Branches: 70.0% 63 / 0 / 90

include/DetourModKit/event_dispatcher.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_EVENT_DISPATCHER_HPP
2 #define DETOURMODKIT_EVENT_DISPATCHER_HPP
3
4 /**
5 * @file event_dispatcher.hpp
6 * @brief Typed event dispatcher with RAII subscription management.
7 *
8 * @details Provides a per-event-type pub/sub dispatcher. Subscribers receive
9 * events by const reference. Subscriptions are RAII objects that
10 * automatically unsubscribe on destruction.
11 *
12 * **Threading model:**
13 * - `emit()` / `emit_safe()` avoid any `shared_mutex` / reader lock.
14 * The zero-subscriber fast path is wait-free: a single
15 * `memory_order_acquire` load of an atomic counter. When
16 * subscribers exist, an atomic acquire-load of a
17 * `std::shared_ptr<const std::vector<Entry>>` snapshot is
18 * performed, then the contiguous handler vector is iterated.
19 * The snapshot load is genuinely lock-free on toolchains that
20 * provide a DWCAS-backed `std::atomic<std::shared_ptr<T>>`
21 * (for example libstdc++ on x86_64), and may use an
22 * implementation-internal short-critical-section bit lock on
23 * toolchains that do not (notably MSVC's STL).
24 * - `subscribe()` / manual `unsubscribe()` serialize writers through
25 * a small `std::mutex` and publish a new immutable snapshot via
26 * copy-on-write. Mutation paths allocate; see the `subscribe()`,
27 * `unsubscribe()`, and `clear()` method docs for the OOM contract.
28 * - Safe to emit from multiple threads concurrently (e.g., hook callbacks).
29 * - Safe to subscribe/unsubscribe from any thread.
30 *
31 * **Performance characteristics:**
32 * - `emit()`: atomic acquire-load of a `shared_ptr` snapshot, then
33 * linear iteration over the contiguous handler vector. No
34 * user-visible mutex acquisition on the hot path. When there are
35 * no subscribers, `emit()` skips the snapshot load entirely via
36 * the atomic counter (wait-free fast path).
37 * - `subscribe()` / `unsubscribe()`: copy-on-write. Each writer
38 * allocates a new handler vector (O(n) in the current subscriber
39 * count), appends or removes an entry, and publishes it atomically.
40 * Typical dispatcher usage is 1-10 subscribers and write-rarely,
41 * so the O(n) publish cost is negligible in practice.
42 * - No heap allocation on `emit()` beyond the `shared_ptr` refcount
43 * bump. Handler vector is cache-friendly.
44 *
45 * **Usage:**
46 * @code
47 * struct PlayerStateChanged { float health; };
48 *
49 * EventDispatcher<PlayerStateChanged> dispatcher;
50 *
51 * // RAII subscription -- auto-unsubscribes when `sub` goes out of scope
52 * auto sub = dispatcher.subscribe([](const PlayerStateChanged& e) {
53 * logger.info("Health: {}", e.health);
54 * });
55 *
56 * // Emit from a hook callback (lock-free, thread-safe)
57 * dispatcher.emit(PlayerStateChanged{.health = 75.0f});
58 * @endcode
59 */
60
61 #include <algorithm>
62 #include <atomic>
63 #include <cstdint>
64 #include <functional>
65 #include <memory>
66 #include <mutex>
67 #include <utility>
68 #include <vector>
69
70 namespace DetourModKit
71 {
72 /**
73 * @brief Opaque subscription identifier returned by EventDispatcher::subscribe().
74 */
75 enum class SubscriptionId : uint64_t
76 {
77 };
78
79 /**
80 * @brief RAII subscription guard that unsubscribes on destruction.
81 *
82 * @details Move-only. When the guard is destroyed or reset, the associated
83 * handler is removed from the dispatcher. If the dispatcher has
84 * already been destroyed, the unsubscribe is silently skipped
85 * (weak_ptr safety).
86 */
87 class Subscription
88 {
89 public:
90 9 Subscription() noexcept = default;
91
92 10281 ~Subscription() noexcept
93 {
94 10281 reset();
95 10281 }
96
97 Subscription(const Subscription &) = delete;
98 Subscription &operator=(const Subscription &) = delete;
99
100 26 Subscription(Subscription &&other) noexcept
101 52 : alive_(std::move(other.alive_)),
102 52 unsubscribe_(std::move(other.unsubscribe_))
103 {
104 26 other.unsubscribe_ = nullptr;
105 26 }
106
107 7 Subscription &operator=(Subscription &&other) noexcept
108 {
109
1/2
✓ Branch 2 → 3 taken 7 times.
✗ Branch 2 → 11 not taken.
7 if (this != &other)
110 {
111 7 reset();
112 14 alive_ = std::move(other.alive_);
113 14 unsubscribe_ = std::move(other.unsubscribe_);
114 7 other.unsubscribe_ = nullptr;
115 }
116 7 return *this;
117 }
118
119 /**
120 * @brief Manually unsubscribes. Safe to call multiple times.
121 * @details If called from within a handler on the same dispatcher
122 * (i.e. emitting_depth > 0 on this thread), the unsubscribe
123 * is silently skipped and the subscription remains active.
124 * The unsubscribe_ lambda is retained so that a subsequent
125 * reset() call outside the emit stack -- including the
126 * Subscription destructor -- will complete the removal.
127 * If the Subscription is also destroyed inside the same
128 * handler scope, the destructor's reset() is likewise
129 * skipped because emitting_depth is still positive.
130 * This keeps the no-mutation-during-emit invariant intact
131 * so the in-flight snapshot iteration remains consistent.
132 */
133 10298 void reset() noexcept
134 {
135
6/6
✓ Branch 3 → 4 taken 10248 times.
✓ Branch 3 → 7 taken 50 times.
✓ Branch 5 → 6 taken 10247 times.
✓ Branch 5 → 7 taken 1 time.
✓ Branch 8 → 9 taken 10247 times.
✓ Branch 8 → 12 taken 51 times.
10298 if (unsubscribe_ && !alive_.expired())
136 {
137
2/2
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 12 taken 10245 times.
10247 if (!unsubscribe_())
138 {
139 2 return;
140 }
141 }
142 10296 unsubscribe_ = nullptr;
143 10296 alive_.reset();
144 }
145
146 /// Returns true if this subscription is still active.
147 9 [[nodiscard]] bool active() const noexcept
148 {
149
4/4
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 7 taken 6 times.
✓ Branch 5 → 6 taken 2 times.
✓ Branch 5 → 7 taken 1 time.
9 return unsubscribe_ != nullptr && !alive_.expired();
150 }
151
152 private:
153 template <typename E>
154 friend class EventDispatcher;
155
156 10246 Subscription(std::weak_ptr<void> alive, std::function<bool()> unsub) noexcept
157 30738 : alive_(std::move(alive)), unsubscribe_(std::move(unsub))
158 {
159 10246 }
160
161 std::weak_ptr<void> alive_;
162 std::function<bool()> unsubscribe_;
163 };
164
165 /**
166 * @brief Thread-safe typed event dispatcher with RAII subscription management.
167 *
168 * @tparam Event The event type. Must be copyable or movable. Handlers receive
169 * events by const reference.
170 *
171 * @details Each EventDispatcher manages a single event type. For multiple event
172 * types, compose multiple dispatchers:
173 * @code
174 * struct MyEvents {
175 * EventDispatcher<PlayerStateChanged> player_state;
176 * EventDispatcher<CameraUpdated> camera;
177 * EventDispatcher<ConfigReloaded> config;
178 * };
179 * @endcode
180 *
181 * **Thread safety:**
182 * - `emit()` / `emit_safe()`: the zero-subscriber fast path is wait-free
183 * (single atomic counter load). Otherwise acquires a `shared_ptr`
184 * snapshot of the immutable handler list and iterates it. The snapshot
185 * load avoids any reader lock; it is lock-free on toolchains with a
186 * DWCAS-backed `std::atomic<std::shared_ptr<T>>` and may use an
187 * implementation-internal bit lock on toolchains that do not.
188 * - `subscribe()` / `unsubscribe()`: copy-on-write under a small writer
189 * mutex. Each mutation allocates a new handler vector, appends or
190 * removes the entry, and publishes the new snapshot atomically. See
191 * the method docs for the OOM contract.
192 * - Handlers are invoked while the snapshot's `shared_ptr` keeps the
193 * vector alive. A thread-local reentrancy guard detects and rejects
194 * subscribe/unsubscribe calls from within a handler; the guard is what
195 * guarantees the user's "do not mutate during emit" invariant, not the
196 * snapshot mechanism.
197 *
198 * **Reentrancy guard scope:** The guard is per-template-instantiation,
199 * not per-instance. Two dispatchers of the same Event type share the
200 * same thread-local counter. Subscribing to a second dispatcher of
201 * the same type from within a handler on the first will be rejected.
202 * Use distinct event types to avoid this (the typical usage pattern).
203 *
204 * **Subscribe/emit ordering invariant:** A subscribe() performs a
205 * release-store on both the snapshot pointer and the atomic handler
206 * count. Any thread that observes the Subscription object returned
207 * from subscribe() (or synchronizes-with the thread that did) will
208 * see the subscription in subsequent emits. Without such a
209 * happens-before edge, a concurrent emit may or may not observe a
210 * freshly-published handler -- this matches the user's own ordering.
211 */
212 template <typename Event>
213 class EventDispatcher
214 {
215 public:
216 /// Handler function signature: receives the event by const reference.
217 using Handler = std::function<void(const Event &)>;
218
219 private:
220 // Private type aliases surfaced here so they are visible to the
221 // public API's member declarations and constructor below.
222 struct Entry
223 {
224 SubscriptionId id;
225 Handler callback;
226 };
227
228 using HandlerList = std::vector<Entry>;
229 using SharedList = std::shared_ptr<const HandlerList>;
230
231 public:
232 29 EventDispatcher()
233
2/4
DetourModKit::EventDispatcher<SimpleEvent>::EventDispatcher():
✓ Branch 2 → 3 taken 27 times.
✗ Branch 2 → 12 not taken.
DetourModKit::EventDispatcher<StringEvent>::EventDispatcher():
✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 12 not taken.
29 : handlers_(std::make_shared<const HandlerList>()),
234
2/4
DetourModKit::EventDispatcher<SimpleEvent>::EventDispatcher():
✓ Branch 8 → 9 taken 27 times.
✗ Branch 8 → 13 not taken.
DetourModKit::EventDispatcher<StringEvent>::EventDispatcher():
✓ Branch 8 → 9 taken 2 times.
✗ Branch 8 → 13 not taken.
29 alive_(std::make_shared<char>('\0'))
235 {
236 29 }
237
238 29 ~EventDispatcher() noexcept = default;
239
240 EventDispatcher(const EventDispatcher &) = delete;
241 EventDispatcher &operator=(const EventDispatcher &) = delete;
242 EventDispatcher(EventDispatcher &&) = delete;
243 EventDispatcher &operator=(EventDispatcher &&) = delete;
244
245 /**
246 * @brief Subscribes a handler to this event type.
247 * @param handler Callable invoked on each emit(). Must be safe to call
248 * from any thread.
249 * @return RAII Subscription guard. The handler is removed when the guard
250 * is destroyed or reset().
251 * @note Copy-on-write: allocates a new handler list of size N+1.
252 * Acceptable for the expected mutation rate (startup and
253 * occasional reconfiguration). Do not call from within a handler.
254 */
255 10249 [[nodiscard]] Subscription subscribe(Handler handler)
256 {
257
3/4
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 5 taken 10244 times.
DetourModKit::EventDispatcher<StringEvent>::subscribe(std::function<void (StringEvent const&)>):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
10249 if (emitting_depth() > 0)
258 {
259 3 return {};
260 }
261
262 const auto id = static_cast<SubscriptionId>(
263 10246 this->next_id_.fetch_add(1, std::memory_order_relaxed));
264
265 {
266
2/4
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 7 → 8 taken 10244 times.
✗ Branch 7 → 55 not taken.
DetourModKit::EventDispatcher<StringEvent>::subscribe(std::function<void (StringEvent const&)>):
✓ Branch 7 → 8 taken 2 times.
✗ Branch 7 → 55 not taken.
10246 std::scoped_lock lock{this->writer_mutex_};
267 10246 auto current = this->handlers_.load(std::memory_order_acquire);
268
2/4
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 10 → 11 taken 10244 times.
✗ Branch 10 → 51 not taken.
DetourModKit::EventDispatcher<StringEvent>::subscribe(std::function<void (StringEvent const&)>):
✓ Branch 10 → 11 taken 2 times.
✗ Branch 10 → 51 not taken.
10246 auto next = std::make_shared<HandlerList>(*current);
269
2/4
DetourModKit::EventDispatcher<SimpleEvent>::subscribe(std::function<void (SimpleEvent const&)>):
✓ Branch 15 → 16 taken 10244 times.
✗ Branch 15 → 46 not taken.
DetourModKit::EventDispatcher<StringEvent>::subscribe(std::function<void (StringEvent const&)>):
✓ Branch 15 → 16 taken 2 times.
✗ Branch 15 → 46 not taken.
20492 next->push_back(Entry{id, std::move(handler)});
270 // Publish the new count first so a reader that sees 0 on the
271 // counter and skips the snapshot load cannot miss a handler
272 // that has already been installed in the snapshot.
273 10246 this->handler_count_.store(next->size(), std::memory_order_release);
274 20492 this->handlers_.store(std::shared_ptr<const HandlerList>(std::move(next)),
275 std::memory_order_release);
276 10246 }
277
278 10246 std::weak_ptr<void> weak = this->alive_;
279 10246 return Subscription(
280 10246 std::move(weak),
281 40985 [this, id]() noexcept -> bool { return this->unsubscribe(id); });
282 30738 }
283
284 /**
285 * @brief Emits an event to all subscribers.
286 * @param event The event payload, passed by const reference to each handler.
287 * @note Lock-free: performs one atomic acquire-load of the snapshot
288 * pointer and iterates. Multiple threads may emit concurrently
289 * without contention. Handlers are invoked synchronously in
290 * subscription order. Exceptions thrown by handlers propagate
291 * to the caller.
292 * @warning If calling from a game hook callback or any context where an
293 * unhandled exception would crash the host process, use
294 * emit_safe() instead. emit() lets handler exceptions propagate
295 * uncaught, which will terminate the process if no catch frame
296 * exists above the call site.
297 */
298 8549 void emit(const Event &event) const
299 {
300 // Fast path: no subscribers means no snapshot load at all.
301
3/4
DetourModKit::EventDispatcher<SimpleEvent>::emit(SimpleEvent const&) const:
✓ Branch 9 → 10 taken 1004 times.
✓ Branch 9 → 11 taken 7611 times.
DetourModKit::EventDispatcher<StringEvent>::emit(StringEvent const&) const:
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 2 times.
17166 if (this->handler_count_.load(std::memory_order_acquire) == 0)
302 {
303 1004 return;
304 }
305
306 7613 SharedList snap = this->handlers_.load(std::memory_order_acquire);
307 8040 EmitGuard guard{emitting_depth()};
308
4/4
DetourModKit::EventDispatcher<SimpleEvent>::emit(SimpleEvent const&) const:
✓ Branch 29 → 17 taken 7871 times.
✓ Branch 29 → 30 taken 7266 times.
DetourModKit::EventDispatcher<StringEvent>::emit(StringEvent const&) const:
✓ Branch 29 → 17 taken 2 times.
✓ Branch 29 → 30 taken 2 times.
23132 for (const auto &entry : *snap)
309 {
310
3/4
DetourModKit::EventDispatcher<SimpleEvent>::emit(SimpleEvent const&) const:
✓ Branch 19 → 20 taken 7777 times.
✓ Branch 19 → 34 taken 1 time.
DetourModKit::EventDispatcher<StringEvent>::emit(StringEvent const&) const:
✓ Branch 19 → 20 taken 2 times.
✗ Branch 19 → 34 not taken.
7873 entry.callback(event);
311 }
312 7270 }
313
314 /**
315 * @brief Emits an event, catching and discarding handler exceptions.
316 * @param event The event payload.
317 * @note Same locking semantics as emit() (lock-free). Handlers that
318 * throw are skipped; remaining handlers still execute.
319 * Prefer this over emit() when calling from hook callbacks or
320 * other contexts where an unhandled exception would crash the
321 * host process.
322 */
323 5252308 void emit_safe(const Event &event) const noexcept
324 {
325
2/2
✓ Branch 9 → 10 taken 170509 times.
✓ Branch 9 → 11 taken 5109762 times.
10532579 if (this->handler_count_.load(std::memory_order_acquire) == 0)
326 {
327 170509 return;
328 }
329
330 // std::shared_ptr copy-construction and load are noexcept, so the
331 // entire function remains noexcept despite the per-handler catch.
332 5109762 SharedList snap = this->handlers_.load(std::memory_order_acquire);
333 5353820 EmitGuard guard{emitting_depth()};
334
2/2
✓ Branch 29 → 17 taken 5239148 times.
✓ Branch 29 → 30 taken 4890118 times.
15463594 for (const auto &entry : *snap)
335 {
336 try
337 {
338
2/2
✓ Branch 19 → 20 taken 5170422 times.
✓ Branch 19 → 34 taken 3 times.
5239148 entry.callback(event);
339 }
340 3 catch (...)
341 {
342 }
343 }
344 4890118 }
345
346 /// Returns the number of active subscribers.
347 19 [[nodiscard]] size_t subscriber_count() const noexcept
348 {
349 38 return this->handler_count_.load(std::memory_order_acquire);
350 }
351
352 /// Returns true if there are no subscribers.
353 4 [[nodiscard]] bool empty() const noexcept
354 {
355 8 return this->handler_count_.load(std::memory_order_acquire) == 0;
356 }
357
358 /**
359 * @brief Removes all subscribers.
360 * @note Serializes with other writers via the writer mutex; readers
361 * in flight keep their snapshot alive through their shared_ptr.
362 * Allocates a fresh empty snapshot. On allocation failure the
363 * dispatcher state is left unchanged (best-effort no-op) so the
364 * noexcept contract is never violated by a throwing allocator.
365 */
366 1 void clear() noexcept
367 {
368 1 std::scoped_lock lock{this->writer_mutex_};
369 // Build the replacement snapshot before touching any published
370 // state so a throwing allocator leaves handlers_ / handler_count_
371 // in their prior consistent pair. Swallowing bad_alloc keeps
372 // clear() a noexcept best-effort teardown.
373 1 std::shared_ptr<const HandlerList> empty_snap;
374 try
375 {
376
1/2
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 30 not taken.
1 empty_snap = std::make_shared<const HandlerList>();
377 }
378 catch (...)
379 {
380 return;
381 }
382 // Counter must go to 0 before publishing the empty snapshot so
383 // an emit that reads 0 on the fast-path counter cannot still see
384 // the non-empty old snapshot afterwards.
385 1 this->handler_count_.store(0, std::memory_order_release);
386 2 this->handlers_.store(std::move(empty_snap), std::memory_order_release);
387
2/4
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 23 not taken.
✓ Branch 25 → 26 taken 1 time.
✗ Branch 25 → 28 not taken.
1 }
388
389 #if defined(DMK_EVENT_DISPATCHER_INTERNAL_TESTING)
390 /**
391 * @brief Test-only diagnostic: returns the number of outstanding
392 * references to the current handler snapshot, excluding the
393 * temporary this call itself creates. A value of 1 means the
394 * dispatcher's own atomic is the sole holder (steady state).
395 * A value >1 indicates an in-flight emit or a leaked snapshot
396 * reference. Enabled only when
397 * DMK_EVENT_DISPATCHER_INTERNAL_TESTING is defined by the
398 * test translation unit. Not part of the public API.
399 */
400 3 [[nodiscard]] long debug_snapshot_use_count() const noexcept
401 {
402 // load() returns a shared_ptr copy that bumps the refcount by 1
403 // for its own lifetime; subtract that so the reported count
404 // reflects only the other holders (the dispatcher atomic and
405 // any in-flight emit snapshots).
406 3 auto snap = this->handlers_.load(std::memory_order_acquire);
407 3 return snap.use_count() - 1;
408 3 }
409 #endif
410
411 private:
412 // Returns false when called from within a handler (reentrancy) or
413 // when the replacement snapshot could not be allocated. The
414 // Subscription::reset() caller retains its unsubscribe_ lambda on
415 // false returns and will retry on the next reset() call (including
416 // the destructor). This is safe because the alive_ weak_ptr prevents
417 // calling into a destroyed dispatcher, and on allocation failure the
418 // published state is left untouched so the retry observes the same
419 // entry still present.
420 //
421 // Allocates (std::make_shared + vector growth). On OOM, leaves the
422 // dispatcher state unchanged and returns false so the RAII retry path
423 // handles it naturally.
424 10247 bool unsubscribe(SubscriptionId id) noexcept
425 {
426
3/4
DetourModKit::EventDispatcher<SimpleEvent>::unsubscribe(DetourModKit::SubscriptionId):
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 5 taken 10243 times.
DetourModKit::EventDispatcher<StringEvent>::unsubscribe(DetourModKit::SubscriptionId):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
10247 if (emitting_depth() > 0)
427 {
428 2 return false;
429 }
430
431 10245 std::scoped_lock lock{this->writer_mutex_};
432 10245 auto current = this->handlers_.load(std::memory_order_acquire);
433 10245 auto it = std::find_if(current->begin(), current->end(),
434 10256 [id](const Entry &e) { return e.id == id; });
435
3/4
DetourModKit::EventDispatcher<SimpleEvent>::unsubscribe(DetourModKit::SubscriptionId):
✓ Branch 20 → 21 taken 2 times.
✓ Branch 20 → 22 taken 10241 times.
DetourModKit::EventDispatcher<StringEvent>::unsubscribe(DetourModKit::SubscriptionId):
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 2 times.
20490 if (it == current->end())
436 {
437 // Not found; treat as successful (idempotent unsubscribe).
438 2 return true;
439 }
440
441 // Build the replacement snapshot in full before touching any
442 // published state. A throwing allocator (reserve / push_back /
443 // make_shared) must not leave handlers_ and handler_count_ out
444 // of sync, and noexcept forbids propagation, so we catch
445 // bad_alloc and fall through to the false-return retry path.
446 10243 std::shared_ptr<HandlerList> next;
447 try
448 {
449
2/4
DetourModKit::EventDispatcher<SimpleEvent>::unsubscribe(DetourModKit::SubscriptionId):
✓ Branch 22 → 23 taken 10241 times.
✗ Branch 22 → 68 not taken.
DetourModKit::EventDispatcher<StringEvent>::unsubscribe(DetourModKit::SubscriptionId):
✓ Branch 22 → 23 taken 2 times.
✗ Branch 22 → 68 not taken.
10243 next = std::make_shared<HandlerList>();
450
2/4
DetourModKit::EventDispatcher<SimpleEvent>::unsubscribe(DetourModKit::SubscriptionId):
✓ Branch 28 → 29 taken 10241 times.
✗ Branch 28 → 70 not taken.
DetourModKit::EventDispatcher<StringEvent>::unsubscribe(DetourModKit::SubscriptionId):
✓ Branch 28 → 29 taken 2 times.
✗ Branch 28 → 70 not taken.
10243 next->reserve(current->size() - 1);
451
4/4
DetourModKit::EventDispatcher<SimpleEvent>::unsubscribe(DetourModKit::SubscriptionId):
✓ Branch 46 → 32 taken 10302 times.
✓ Branch 46 → 47 taken 10241 times.
DetourModKit::EventDispatcher<StringEvent>::unsubscribe(DetourModKit::SubscriptionId):
✓ Branch 46 → 32 taken 2 times.
✓ Branch 46 → 47 taken 2 times.
30790 for (const auto &entry : *current)
452 {
453
3/4
DetourModKit::EventDispatcher<SimpleEvent>::unsubscribe(DetourModKit::SubscriptionId):
✓ Branch 34 → 35 taken 61 times.
✓ Branch 34 → 37 taken 10241 times.
DetourModKit::EventDispatcher<StringEvent>::unsubscribe(DetourModKit::SubscriptionId):
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 37 taken 2 times.
10304 if (entry.id != id)
454 {
455
1/4
DetourModKit::EventDispatcher<SimpleEvent>::unsubscribe(DetourModKit::SubscriptionId):
✓ Branch 36 → 37 taken 61 times.
✗ Branch 36 → 69 not taken.
DetourModKit::EventDispatcher<StringEvent>::unsubscribe(DetourModKit::SubscriptionId):
✗ Branch 36 → 37 not taken.
✗ Branch 36 → 69 not taken.
61 next->push_back(entry);
456 }
457 }
458 }
459 catch (...)
460 {
461 return false;
462 }
463
464 // Publish snapshot first, then the counter. An emit that loads a
465 // stale snapshot containing the removed handler is still safe
466 // because the handler callable is retained by the old snapshot.
467 20486 this->handlers_.store(std::shared_ptr<const HandlerList>(std::move(next)),
468 std::memory_order_release);
469 10243 this->handler_count_.store(current->size() - 1, std::memory_order_release);
470 10243 return true;
471 10245 }
472
473 // Thread-local emit depth counter. This is per-template-instantiation
474 // (not per-instance) because making it per-instance would require a
475 // thread_local map keyed by this pointer, adding a hash lookup to
476 // every emit() hot path. The typical usage is one dispatcher per
477 // event type, so the shared counter is the correct tradeoff. See
478 // the class-level doc for details.
479 5372437 [[nodiscard]] int &emitting_depth() const noexcept
480 {
481 // Shared across all dispatcher instances on the same thread.
482 // The reentrancy guard is per-thread (intentional), not per-dispatcher.
483 thread_local int depth{0};
484 5372437 return depth;
485 }
486
487 /// RAII guard that increments/decrements the emit depth counter.
488 struct EmitGuard
489 {
490 int &depth;
491 5342664 explicit EmitGuard(int &d) noexcept : depth(d) { ++depth; }
492 5141180 ~EmitGuard() noexcept { --depth; }
493 EmitGuard(const EmitGuard &) = delete;
494 EmitGuard &operator=(const EmitGuard &) = delete;
495 EmitGuard(EmitGuard &&) = delete;
496 EmitGuard &operator=(EmitGuard &&) = delete;
497 };
498
499 // alignas(64) keeps the hot atomics on their own cache line so the
500 // writer mutex and shared_ptr control-block traffic do not produce
501 // false sharing with readers doing the fast-path counter load.
502 alignas(64) mutable std::atomic<SharedList> handlers_;
503 std::atomic<size_t> handler_count_{0};
504 std::atomic<uint64_t> next_id_{1};
505 std::mutex writer_mutex_; // serializes writers only
506 std::shared_ptr<void> alive_; // Prevents Subscription::reset() from calling
507 // unsubscribe() after dispatcher destruction.
508 };
509
510 } // namespace DetourModKit
511
512 #endif // DETOURMODKIT_EVENT_DISPATCHER_HPP
513