Skip to content

Commit

Permalink
Replace ListenerHandle with uint64_t, introduce RAII Token type
Browse files Browse the repository at this point in the history
  • Loading branch information
KyrietS committed Feb 24, 2024
1 parent d3ee7ff commit 37315f5
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 39 deletions.
40 changes: 30 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

*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.

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.
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 `tinyevents::Token` helper class you can get RAII-style automatic listener removal.

## Basic Usage

Expand All @@ -21,15 +21,22 @@ struct MyEvent {
int main() {
tinyevents::Dispatcher dispatcher;

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

dispatcher.queue(MyEvent{77});
dispatcher.dispatch(MyEvent{42}); // Prints "Received MyEvent: 42"
dispatcher.process(); // Prints "Received MyEvent: 77"
// Dispatch an event
dispatcher.dispatch(MyEvent{11}); // Prints "Received MyEvent: 11"

dispatcher.dispatch(123); // No listener for this event, so nothing happens
// Queue events
dispatcher.queue(MyEvent{22});
dispatcher.queue(MyEvent{33});
dispatcher.process(); // Prints "Received MyEvent: 22"
// "Received MyEvent: 33"

dispatcher.remove(handle); // Remove the listener
dispatcher.dispatch(MyEvent{44}); // No listener, so nothing happens

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

```cpp
template<typename Event>
ListenerHandle listen(const std::function<void(const Event&)>& listener)
std::uint64_t listen(const std::function<void(const Event&)>& listener)
```
* listener - A callable object that will be called when an event of type `Event` is dispatched. The object must be copyable.
Expand All @@ -82,7 +89,7 @@ Same as `listen()`, but the listener will be removed after it is called once.
```cpp
template<typename Event>
ListenerHandle listenOnce(const std::function<void(const Event&)>& listener)
std::uint64_t listenOnce(const std::function<void(const Event&)>& listener)
```
* listener - A callable object that will be called when an event of type `Event` is dispatched. The object must be copyable.

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

```cpp
void remove(const ListenerHandle& handle)
void remove(std::uint64_t handle)
```
* handle - The handle of the listener to remove.
Expand All @@ -102,7 +109,7 @@ void remove(const ListenerHandle& handle)
Check if a listener is registered in the dispatcher.
```cpp
bool hasListener(const ListenerHandle& handle)
bool hasListener(std::uint64_t handle)
```
* handle - The handle of the listener to check.

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

## Token - helper RAII class
```cpp

tinevents::Dispatcher dispatcher;
std::uint64_t handle = dispatcher.listen<MyEvent>(myCallback);

// RAII token
tinyevents::Token token(dispatcher, handle); // When this token goes out of scope the listener
// will be automatically removed from dispatcher.
```
Make sure that the dispatcher is still alive when the token is destroyed.
## Tests
Tests are written using Google Test. The library is fetched automatically by CMake during the configuration step of the tests.
Expand Down
80 changes: 56 additions & 24 deletions include/tinyevents/tinyevents.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,10 @@

namespace tinyevents
{
class ListenerHandle {
public:
explicit ListenerHandle(std::uint64_t id) : id(id) {}

[[nodiscard]] std::uint64_t value() const { return id; }

friend constexpr bool operator== (const ListenerHandle& lhs, const ListenerHandle& rhs) {
return lhs.id == rhs.id;
}
friend constexpr bool operator!= (const ListenerHandle& lhs, const ListenerHandle& rhs) {
return lhs.id != rhs.id;
}
friend constexpr bool operator< (const ListenerHandle& lhs, const ListenerHandle& rhs) {
return lhs.id < rhs.id;
}

private:
std::uint64_t id;
};
class Token;

class Dispatcher {
using ListenerHandle = std::uint64_t;
using Listeners = std::map<ListenerHandle, std::function<void(const void *)>>;
public:
Dispatcher() = default;
Expand All @@ -38,7 +21,7 @@ namespace tinyevents
Dispatcher &operator=(Dispatcher &&) noexcept = default;

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

Expand All @@ -50,7 +33,7 @@ namespace tinyevents
}

template<typename T>
ListenerHandle listenOnce(const std::function<void(const T &)> &listener) {
std::uint64_t listenOnce(const std::function<void(const T &)> &listener) {
const auto listenerId = nextListenerId;
return listen<T>([this, listenerId, listener](const T &msg) {
ListenerHandle handle{listenerId};
Expand Down Expand Up @@ -101,7 +84,7 @@ namespace tinyevents
queuedDispatches.clear();
}

void remove(const ListenerHandle &handle) {
void remove(const std::uint64_t handle) {
if (isScheduledForRemoval(handle)) {
return;
}
Expand All @@ -111,7 +94,7 @@ namespace tinyevents
}
}

[[nodiscard]] bool hasListener(const ListenerHandle& handle) const {
[[nodiscard]] bool hasListener(std::uint64_t handle) const {
if (isScheduledForRemoval(handle)) {
return false;
}
Expand All @@ -122,7 +105,7 @@ namespace tinyevents
}

private:
bool isScheduledForRemoval(const ListenerHandle& handle) const {
[[nodiscard]] bool isScheduledForRemoval(const std::uint64_t handle) const {
return listenersScheduledForRemoval.find(handle) != listenersScheduledForRemoval.end();
}

Expand All @@ -132,4 +115,53 @@ namespace tinyevents

std::uint64_t nextListenerId = 0;
};

// RAII wrapper for listener handle.
class Token {
public:
Token(Dispatcher& dispatcher, const std::uint64_t handle)
: dispatcher(dispatcher), _handle(handle), holdsResource(true) {}
~Token() {
if (holdsResource) {
dispatcher.get().remove(_handle);
}
}

// Disable copy operations
Token(const Token&) = delete;
Token& operator=(const Token&) = delete;

// Enable move operations
Token(Token&& other) noexcept
: dispatcher(other.dispatcher), _handle(other._handle), holdsResource(other.holdsResource) {
other.holdsResource = false;
}

Token& operator=(Token&& other) noexcept {
if (this != &other) {
if (this->holdsResource) {
dispatcher.get().remove(_handle);
}
dispatcher = other.dispatcher;
_handle = other._handle;
holdsResource = other.holdsResource;
other.holdsResource = false;
}
return *this;
}

[[nodiscard]] std::uint64_t handle() const {
return _handle;
}

void remove() {
dispatcher.get().remove(_handle);
holdsResource = false;
}

private:
std::reference_wrapper<Dispatcher> dispatcher;
std::uint64_t _handle;
bool holdsResource;
};
}// namespace tinyevents
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ add_executable(${TEST_TARGET}
TestEventQueue.cpp
TestEventDispatcherMove.cpp
TestEventListen.cpp
TestToken.cpp
)

# tinyevents
Expand Down
31 changes: 26 additions & 5 deletions tests/TestEventListen.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ struct TestEventListen : public Test {


TEST_F(TestEventListen, VerifyListenerHandlePredicates) {
// Get the type of value returned by listen
using ListenerHandle = decltype(std::declval<Dispatcher>().listen<int>(nullptr));

// Check if Handle is movable
EXPECT_TRUE(std::is_move_constructible_v<ListenerHandle>);
EXPECT_TRUE(std::is_move_assignable_v<ListenerHandle>);
Expand All @@ -29,7 +32,7 @@ TEST_F(TestEventListen, VerifyListenerHandlePredicates) {
TEST_F(TestEventListen, AddingNewListenerShouldReturnHandle) {
StrictMock<MockFunction<void(const int &)>> callback;

EXPECT_THAT(dispatcher.listen(callback.AsStdFunction()), A<ListenerHandle>());
EXPECT_THAT(dispatcher.listen(callback.AsStdFunction()), A<std::uint64_t>());
}

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

EXPECT_NE(handle1, handle2);
EXPECT_NE(handle1.value(), handle2.value());
}

TEST_F(TestEventListen, ReturnedHandleShouldBeInvalidAfterRemoval) {
Expand Down Expand Up @@ -92,7 +94,7 @@ TEST_F(TestEventListen, ListenersCanAddAnotherListener) {
TEST_F(TestEventListen, ListenerCanRemoveItself) {
StrictMock<MockFunction<void(const int &)>> callback;

ListenerHandle handleToSelf{0};
std::uint64_t handleToSelf{0};

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

ListenerHandle handle2{0};
std::uint64_t handle2{0};

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

ListenerHandle handleToSelf{0};
std::uint64_t handleToSelf{0};

handleToSelf = dispatcher.listenOnce<int>([&](const int &value) {
callback.Call(value);
Expand All @@ -185,4 +187,23 @@ TEST_F(TestEventListen, ListenerOnceShouldBeCalledOnceEvenIfMessageIsSentFromLis
dispatcher.dispatch(111);
EXPECT_FALSE(dispatcher.hasListener(handle));
dispatcher.dispatch(333);
}

TEST_F(TestEventListen, TestRaiiApproachToListenerRemoval) {
// Prepare a unique_ptr type that will provide RAII behavior
auto deleter = [this](const std::uint64_t* raiiHandle) {
dispatcher.remove(*raiiHandle);
delete raiiHandle;
};
using RaiiHandleType = std::unique_ptr<std::uint64_t, decltype(deleter)>;

StrictMock<MockFunction<void(const int&)>> callback;
const auto handle = dispatcher.listen<int>(callback.AsStdFunction());

// Inner scope to test RAII behavior
{
EXPECT_TRUE(dispatcher.hasListener(handle));
RaiiHandleType raiiHandle(new std::uint64_t(handle), deleter);
}
EXPECT_FALSE(dispatcher.hasListener(handle));
}
67 changes: 67 additions & 0 deletions tests/TestToken.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <tinyevents/tinyevents.hpp>

using namespace tinyevents;
using namespace testing;

struct TestToken : public Test {
Dispatcher dispatcher{};
};

TEST(TestTokenMove, VerifyTokenPredicates) {
// Check if Token is movable
EXPECT_TRUE(std::is_move_constructible_v<Token>);
EXPECT_TRUE(std::is_move_assignable_v<Token>);

// Check if Token is NOT copyable
EXPECT_FALSE(std::is_copy_constructible_v<Token>);
EXPECT_FALSE(std::is_copy_assignable_v<Token>);
}

TEST_F(TestToken, WhenTokenIsDeletedThenHandleIsRemovedFromDispatcher) {
const auto handle = dispatcher.listen<int>(nullptr);

// Inner scope to test RAII behavior
{
EXPECT_TRUE(dispatcher.hasListener(handle));
const Token token{dispatcher, handle};
EXPECT_EQ(token.handle(), handle);
}
EXPECT_FALSE(dispatcher.hasListener(handle));
}

TEST_F(TestToken, WhenTokenIsRemovedManuallyThenHandleIsRemovedFromDispatcher) {
const auto handle = dispatcher.listen<int>(nullptr);

Token token{dispatcher, handle};
EXPECT_TRUE(dispatcher.hasListener(handle));
token.remove();
EXPECT_FALSE(dispatcher.hasListener(handle));
EXPECT_EQ(token.handle(), handle); // Handle should not be modified
}

TEST_F(TestToken, WhenTokenIsMovedConstructedThenHandleIsNotRemovedFromDispatcher) {
const auto handle = dispatcher.listen<int>(nullptr);

const auto token1 = new Token(dispatcher, handle);
const auto token2 = new Token(std::move(*token1));
delete token1;
EXPECT_TRUE(dispatcher.hasListener(handle));

delete token2;
EXPECT_FALSE(dispatcher.hasListener(handle));
}

TEST_F(TestToken, WhenTokenIsMovedAssignedThenHandleIsNotRemovedFromDispatcher) {
const auto handle1 = dispatcher.listen<int>(nullptr);
const auto handle2 = dispatcher.listen<int>(nullptr);

const auto token1 = new Token(dispatcher, handle1);
const auto token2 = new Token(dispatcher, handle2);

*token2 = std::move(*token1);
delete token1;
ASSERT_TRUE(dispatcher.hasListener(handle1));
ASSERT_FALSE(dispatcher.hasListener(handle2));
}

0 comments on commit 37315f5

Please sign in to comment.