Skip to content

Commit 7b457c7

Browse files
committed
Replace ListenerHandle with uint64_t, introduce RAII Token type
1 parent d3ee7ff commit 7b457c7

File tree

5 files changed

+181
-40
lines changed

5 files changed

+181
-40
lines changed

README.md

+31-11
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# TinyEvents - A Simple Event-Dispatcher System for C++
22

3-
<!--[![Mizeria release](https://img.shields.io/github/v/release/KyrietS/tinyevents?include_prereleases&sort=semver)](https://github.com/KyrietS/tinyevents/releases)-->
3+
<!--[![release](https://img.shields.io/github/v/release/KyrietS/tinyevents?include_prereleases&sort=semver)](https://github.com/KyrietS/tinyevents/releases)-->
44
[![Tests](https://github.com/KyrietS/tinyevents/actions/workflows/tests.yml/badge.svg)](https://github.com/KyrietS/tinyevents/actions/workflows/tests.yml)
55
[![Lincense](https://img.shields.io/github/license/KyrietS/tinyevents)](LICENSE)
66

77
*TinyEvents* is a simple header-only library for C++ that provides a basic, yet powerfull, event-dispatcher system. It is designed to be easy to use and to have minimal dependencies. It is written in C++17 and has no dependencies other than the standard library.
88

9-
In *TinyEvents* any type can be used as an event. The events are dispatched to listeners that are registered for a specific event type. Asynchronous (deferred) dispatching using a queue is also supported.
9+
In *TinyEvents* any type can be used as an event. The events are dispatched to listeners that are registered for a specific event type. Asynchronous (deferred) dispatching using a queue is also supported. With the `tinyevents::Token` helper class you can get RAII-style automatic listener removal.
1010

1111
## Basic Usage
1212

@@ -21,15 +21,22 @@ struct MyEvent {
2121
int main() {
2222
tinyevents::Dispatcher dispatcher;
2323

24-
dispatcher.listen<MyEvent>([](const auto& event) {
24+
// Register a listener for MyEvent
25+
auto handle = dispatcher.listen<MyEvent>([](const auto& event) {
2526
std::cout << "Received MyEvent: " << event.value << std::endl;
2627
});
2728

28-
dispatcher.queue(MyEvent{77});
29-
dispatcher.dispatch(MyEvent{42}); // Prints "Received MyEvent: 42"
30-
dispatcher.process(); // Prints "Received MyEvent: 77"
29+
// Dispatch an event
30+
dispatcher.dispatch(MyEvent{11}); // Prints "Received MyEvent: 11"
3131

32-
dispatcher.dispatch(123); // No listener for this event, so nothing happens
32+
// Queue events
33+
dispatcher.queue(MyEvent{22});
34+
dispatcher.queue(MyEvent{33});
35+
dispatcher.process(); // Prints "Received MyEvent: 22"
36+
// "Received MyEvent: 33"
37+
38+
dispatcher.remove(handle); // Remove the listener
39+
dispatcher.dispatch(MyEvent{44}); // No listener, so nothing happens
3340

3441
return 0;
3542
}
@@ -70,7 +77,7 @@ Register a listener for a specific event type. The listener will be called when
7077

7178
```cpp
7279
template<typename Event>
73-
ListenerHandle listen(const std::function<void(const Event&)>& listener)
80+
std::uint64_t listen(const std::function<void(const Event&)>& listener)
7481
```
7582
* listener - A callable object that will be called when an event of type `Event` is dispatched. The object must be copyable.
7683
@@ -82,7 +89,7 @@ Same as `listen()`, but the listener will be removed after it is called once.
8289
8390
```cpp
8491
template<typename Event>
85-
ListenerHandle listenOnce(const std::function<void(const Event&)>& listener)
92+
std::uint64_t listenOnce(const std::function<void(const Event&)>& listener)
8693
```
8794
* listener - A callable object that will be called when an event of type `Event` is dispatched. The object must be copyable.
8895

@@ -93,7 +100,7 @@ Returns a handle that can be used to remove the listener.
93100
Remove a listener that was previously registered using `listen()`.
94101

95102
```cpp
96-
void remove(const ListenerHandle& handle)
103+
void remove(std::uint64_t handle)
97104
```
98105
* handle - The handle of the listener to remove.
99106
@@ -102,7 +109,7 @@ void remove(const ListenerHandle& handle)
102109
Check if a listener is registered in the dispatcher.
103110
104111
```cpp
105-
bool hasListener(const ListenerHandle& handle)
112+
bool hasListener(std::uint64_t handle)
106113
```
107114
* handle - The handle of the listener to check.
108115

@@ -142,6 +149,19 @@ void process()
142149
* You can safely call `remove(handle)` for a handle that was already removed. Nothing will happen.
143150
* Handles are never reused by the same dispatcher.
144151

152+
## Token - helper RAII class
153+
```cpp
154+
155+
tinevents::Dispatcher dispatcher;
156+
std::uint64_t handle = dispatcher.listen<MyEvent>(myCallback);
157+
158+
// RAII token
159+
tinyevents::Token token(dispatcher, handle); // When this token goes out of scope the listener
160+
// will be automatically removed from dispatcher.
161+
```
162+
163+
Make sure that the dispatcher is still alive when the token is destroyed.
164+
145165
## Tests
146166
147167
Tests are written using Google Test. The library is fetched automatically by CMake during the configuration step of the tests.

include/tinyevents/tinyevents.hpp

+56-24
Original file line numberDiff line numberDiff line change
@@ -9,27 +9,10 @@
99

1010
namespace tinyevents
1111
{
12-
class ListenerHandle {
13-
public:
14-
explicit ListenerHandle(std::uint64_t id) : id(id) {}
15-
16-
[[nodiscard]] std::uint64_t value() const { return id; }
17-
18-
friend constexpr bool operator== (const ListenerHandle& lhs, const ListenerHandle& rhs) {
19-
return lhs.id == rhs.id;
20-
}
21-
friend constexpr bool operator!= (const ListenerHandle& lhs, const ListenerHandle& rhs) {
22-
return lhs.id != rhs.id;
23-
}
24-
friend constexpr bool operator< (const ListenerHandle& lhs, const ListenerHandle& rhs) {
25-
return lhs.id < rhs.id;
26-
}
27-
28-
private:
29-
std::uint64_t id;
30-
};
12+
class Token;
3113

3214
class Dispatcher {
15+
using ListenerHandle = std::uint64_t;
3316
using Listeners = std::map<ListenerHandle, std::function<void(const void *)>>;
3417
public:
3518
Dispatcher() = default;
@@ -38,7 +21,7 @@ namespace tinyevents
3821
Dispatcher &operator=(Dispatcher &&) noexcept = default;
3922

4023
template<typename T>
41-
ListenerHandle listen(const std::function<void(const T &)> &listener) {
24+
std::uint64_t listen(const std::function<void(const T &)> &listener) {
4225
auto& listeners = listenersByType[std::type_index(typeid(T))];
4326
const auto listenerHandle = ListenerHandle{nextListenerId++};
4427

@@ -50,7 +33,7 @@ namespace tinyevents
5033
}
5134

5235
template<typename T>
53-
ListenerHandle listenOnce(const std::function<void(const T &)> &listener) {
36+
std::uint64_t listenOnce(const std::function<void(const T &)> &listener) {
5437
const auto listenerId = nextListenerId;
5538
return listen<T>([this, listenerId, listener](const T &msg) {
5639
ListenerHandle handle{listenerId};
@@ -101,7 +84,7 @@ namespace tinyevents
10184
queuedDispatches.clear();
10285
}
10386

104-
void remove(const ListenerHandle &handle) {
87+
void remove(const std::uint64_t handle) {
10588
if (isScheduledForRemoval(handle)) {
10689
return;
10790
}
@@ -111,7 +94,7 @@ namespace tinyevents
11194
}
11295
}
11396

114-
[[nodiscard]] bool hasListener(const ListenerHandle& handle) const {
97+
[[nodiscard]] bool hasListener(std::uint64_t handle) const {
11598
if (isScheduledForRemoval(handle)) {
11699
return false;
117100
}
@@ -122,7 +105,7 @@ namespace tinyevents
122105
}
123106

124107
private:
125-
bool isScheduledForRemoval(const ListenerHandle& handle) const {
108+
[[nodiscard]] bool isScheduledForRemoval(const std::uint64_t handle) const {
126109
return listenersScheduledForRemoval.find(handle) != listenersScheduledForRemoval.end();
127110
}
128111

@@ -132,4 +115,53 @@ namespace tinyevents
132115

133116
std::uint64_t nextListenerId = 0;
134117
};
118+
119+
// RAII wrapper for listener handle.
120+
class Token {
121+
public:
122+
Token(Dispatcher& dispatcher, const std::uint64_t handle)
123+
: dispatcher(dispatcher), _handle(handle), holdsResource(true) {}
124+
~Token() {
125+
if (holdsResource) {
126+
dispatcher.get().remove(_handle);
127+
}
128+
}
129+
130+
// Disable copy operations
131+
Token(const Token&) = delete;
132+
Token& operator=(const Token&) = delete;
133+
134+
// Enable move operations
135+
Token(Token&& other) noexcept
136+
: dispatcher(other.dispatcher), _handle(other._handle), holdsResource(other.holdsResource) {
137+
other.holdsResource = false;
138+
}
139+
140+
Token& operator=(Token&& other) noexcept {
141+
if (this != &other) {
142+
if (this->holdsResource) {
143+
dispatcher.get().remove(_handle);
144+
}
145+
dispatcher = other.dispatcher;
146+
_handle = other._handle;
147+
holdsResource = other.holdsResource;
148+
other.holdsResource = false;
149+
}
150+
return *this;
151+
}
152+
153+
[[nodiscard]] std::uint64_t handle() const {
154+
return _handle;
155+
}
156+
157+
void remove() {
158+
dispatcher.get().remove(_handle);
159+
holdsResource = false;
160+
}
161+
162+
private:
163+
std::reference_wrapper<Dispatcher> dispatcher;
164+
std::uint64_t _handle;
165+
bool holdsResource;
166+
};
135167
}// namespace tinyevents

tests/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ add_executable(${TEST_TARGET}
1919
TestEventQueue.cpp
2020
TestEventDispatcherMove.cpp
2121
TestEventListen.cpp
22+
TestToken.cpp
2223
)
2324

2425
# tinyevents

tests/TestEventListen.cpp

+26-5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ struct TestEventListen : public Test {
1111

1212

1313
TEST_F(TestEventListen, VerifyListenerHandlePredicates) {
14+
// Get the type of value returned by listen
15+
using ListenerHandle = decltype(std::declval<Dispatcher>().listen<int>(nullptr));
16+
1417
// Check if Handle is movable
1518
EXPECT_TRUE(std::is_move_constructible_v<ListenerHandle>);
1619
EXPECT_TRUE(std::is_move_assignable_v<ListenerHandle>);
@@ -29,7 +32,7 @@ TEST_F(TestEventListen, VerifyListenerHandlePredicates) {
2932
TEST_F(TestEventListen, AddingNewListenerShouldReturnHandle) {
3033
StrictMock<MockFunction<void(const int &)>> callback;
3134

32-
EXPECT_THAT(dispatcher.listen(callback.AsStdFunction()), A<ListenerHandle>());
35+
EXPECT_THAT(dispatcher.listen(callback.AsStdFunction()), A<std::uint64_t>());
3336
}
3437

3538
TEST_F(TestEventListen, ReturnedHandleShouldBeValid) {
@@ -46,7 +49,6 @@ TEST_F(TestEventListen, ReturnedHandlesShouldBeDifferent) {
4649
const auto handle2 = dispatcher.listen(callback.AsStdFunction());
4750

4851
EXPECT_NE(handle1, handle2);
49-
EXPECT_NE(handle1.value(), handle2.value());
5052
}
5153

5254
TEST_F(TestEventListen, ReturnedHandleShouldBeInvalidAfterRemoval) {
@@ -92,7 +94,7 @@ TEST_F(TestEventListen, ListenersCanAddAnotherListener) {
9294
TEST_F(TestEventListen, ListenerCanRemoveItself) {
9395
StrictMock<MockFunction<void(const int &)>> callback;
9496

95-
ListenerHandle handleToSelf{0};
97+
std::uint64_t handleToSelf{0};
9698

9799
handleToSelf = dispatcher.listen<int>([&](const int &value) {
98100
callback.Call(value);
@@ -110,7 +112,7 @@ TEST_F(TestEventListen, ListenerCanRemoveAnotherListener) {
110112
StrictMock<MockFunction<void(const int &)>> callback1;
111113
StrictMock<MockFunction<void(const int &)>> callback2;
112114

113-
ListenerHandle handle2{0};
115+
std::uint64_t handle2{0};
114116

115117
dispatcher.listen<int>([&](const int &value) {
116118
callback1.Call(value);
@@ -159,7 +161,7 @@ TEST_F(TestEventListen, ListenOnceCanBeCalledFromInsideAnotherListenOnceCallback
159161
TEST_F(TestEventListen, ListenerOnceShouldBeRemovedAfterCallEvenIfItRemovesItself) {
160162
StrictMock<MockFunction<void(const int &)>> callback;
161163

162-
ListenerHandle handleToSelf{0};
164+
std::uint64_t handleToSelf{0};
163165

164166
handleToSelf = dispatcher.listenOnce<int>([&](const int &value) {
165167
callback.Call(value);
@@ -185,4 +187,23 @@ TEST_F(TestEventListen, ListenerOnceShouldBeCalledOnceEvenIfMessageIsSentFromLis
185187
dispatcher.dispatch(111);
186188
EXPECT_FALSE(dispatcher.hasListener(handle));
187189
dispatcher.dispatch(333);
190+
}
191+
192+
TEST_F(TestEventListen, TestRaiiApproachToListenerRemoval) {
193+
// Prepare a unique_ptr type that will provide RAII behavior
194+
auto deleter = [this](const std::uint64_t* raiiHandle) {
195+
dispatcher.remove(*raiiHandle);
196+
delete raiiHandle;
197+
};
198+
using RaiiHandleType = std::unique_ptr<std::uint64_t, decltype(deleter)>;
199+
200+
StrictMock<MockFunction<void(const int&)>> callback;
201+
const auto handle = dispatcher.listen<int>(callback.AsStdFunction());
202+
203+
// Inner scope to test RAII behavior
204+
{
205+
EXPECT_TRUE(dispatcher.hasListener(handle));
206+
RaiiHandleType raiiHandle(new std::uint64_t(handle), deleter);
207+
}
208+
EXPECT_FALSE(dispatcher.hasListener(handle));
188209
}

tests/TestToken.cpp

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
#include <gmock/gmock.h>
2+
#include <gtest/gtest.h>
3+
#include <tinyevents/tinyevents.hpp>
4+
5+
using namespace tinyevents;
6+
using namespace testing;
7+
8+
struct TestToken : public Test {
9+
Dispatcher dispatcher{};
10+
};
11+
12+
TEST(TestTokenMove, VerifyTokenPredicates) {
13+
// Check if Token is movable
14+
EXPECT_TRUE(std::is_move_constructible_v<Token>);
15+
EXPECT_TRUE(std::is_move_assignable_v<Token>);
16+
17+
// Check if Token is NOT copyable
18+
EXPECT_FALSE(std::is_copy_constructible_v<Token>);
19+
EXPECT_FALSE(std::is_copy_assignable_v<Token>);
20+
}
21+
22+
TEST_F(TestToken, WhenTokenIsDeletedThenHandleIsRemovedFromDispatcher) {
23+
const auto handle = dispatcher.listen<int>(nullptr);
24+
25+
// Inner scope to test RAII behavior
26+
{
27+
EXPECT_TRUE(dispatcher.hasListener(handle));
28+
const Token token{dispatcher, handle};
29+
EXPECT_EQ(token.handle(), handle);
30+
}
31+
EXPECT_FALSE(dispatcher.hasListener(handle));
32+
}
33+
34+
TEST_F(TestToken, WhenTokenIsRemovedManuallyThenHandleIsRemovedFromDispatcher) {
35+
const auto handle = dispatcher.listen<int>(nullptr);
36+
37+
Token token{dispatcher, handle};
38+
EXPECT_TRUE(dispatcher.hasListener(handle));
39+
token.remove();
40+
EXPECT_FALSE(dispatcher.hasListener(handle));
41+
EXPECT_EQ(token.handle(), handle); // Handle should not be modified
42+
}
43+
44+
TEST_F(TestToken, WhenTokenIsMovedConstructedThenHandleIsNotRemovedFromDispatcher) {
45+
const auto handle = dispatcher.listen<int>(nullptr);
46+
47+
const auto token1 = new Token(dispatcher, handle);
48+
const auto token2 = new Token(std::move(*token1));
49+
delete token1;
50+
EXPECT_TRUE(dispatcher.hasListener(handle));
51+
52+
delete token2;
53+
EXPECT_FALSE(dispatcher.hasListener(handle));
54+
}
55+
56+
TEST_F(TestToken, WhenTokenIsMovedAssignedThenHandleIsNotRemovedFromDispatcher) {
57+
const auto handle1 = dispatcher.listen<int>(nullptr);
58+
const auto handle2 = dispatcher.listen<int>(nullptr);
59+
60+
const auto token1 = new Token(dispatcher, handle1);
61+
const auto token2 = new Token(dispatcher, handle2);
62+
63+
*token2 = std::move(*token1);
64+
delete token1;
65+
ASSERT_TRUE(dispatcher.hasListener(handle1));
66+
ASSERT_FALSE(dispatcher.hasListener(handle2));
67+
}

0 commit comments

Comments
 (0)