Created
October 19, 2022 10:26
-
-
Save anthonyprintup/4f28644d5028649a77e02eb33faa4177 to your computer and use it in GitHub Desktop.
Event listener (signals) implementation.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#pragma once | |
namespace events { | |
struct Event {}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#pragma once | |
#include <vector> | |
#include <ranges> | |
#include <concepts> | |
#include <functional> | |
#include "event.hpp" | |
#include "priority.hpp" | |
namespace events { | |
template<std::derived_from<Event> InputEventType> | |
struct Listener { | |
using EventType = InputEventType; | |
using CallbackType = std::function<void(EventType&)>; | |
using IdentityType = std::size_t; | |
using DataType = std::tuple<IdentityType, Priority, CallbackType>; | |
using FunctionCollectionType = std::vector<DataType>; | |
template<class Functor> | |
requires std::is_invocable_v<Functor, EventType&> | |
constexpr IdentityType add_callback(Functor &&callback, const Priority priority = Priority::DEFAULT) { | |
const auto iterator = std::ranges::find_if(std::as_const(this->callbacks), | |
Listener::priority_less_than(priority)); | |
const auto identity = this->generate_new_identity(); | |
this->callbacks.emplace(iterator, identity, priority, std::forward<Functor>(callback)); | |
return identity; | |
} | |
constexpr void remove_callback(const IdentityType identity) { | |
std::erase_if(this->callbacks, Listener::identity_equal_to(identity)); | |
} | |
constexpr EventType &invoke_callbacks(EventType &event) const { | |
for (auto &&callback : this->callbacks | std::views::transform(Listener::callback_transformer)) | |
callback(event); | |
return event; | |
} | |
template<class... Arguments> | |
requires std::is_constructible_v<EventType, Arguments...> or std::is_aggregate_v<EventType> | |
constexpr EventType invoke_callbacks(Arguments&&... arguments) const { | |
EventType event {Event {}, std::forward<Arguments>(arguments)...}; | |
return this->invoke_callbacks(event); | |
} | |
private: | |
template<class ElementType> | |
static constexpr decltype(auto) transformer = | |
[][[nodiscard]](const DataType &entry) constexpr noexcept -> decltype(auto) { | |
return std::get<ElementType>(entry); | |
}; | |
static constexpr auto identity_transformer = Listener::transformer<IdentityType>; | |
static constexpr auto callback_transformer = Listener::transformer<CallbackType>; | |
template<class ElementType, class Comparator> | |
static constexpr decltype(auto) comparator = | |
[][[nodiscard]](const DataType &entry, const ElementType element) constexpr noexcept { | |
return Comparator {}(std::get<ElementType>(entry), element); | |
}; | |
template<class ElementType, class Comparator> | |
static constexpr decltype(auto) bound_comparator = | |
[][[nodiscard]](const ElementType element) constexpr noexcept { | |
using namespace std::placeholders; | |
return std::bind(comparator<ElementType, Comparator>, _1, element); | |
}; | |
static constexpr auto identity_equal_to = Listener::bound_comparator<IdentityType, std::ranges::equal_to>; | |
static constexpr auto priority_less_than = Listener::bound_comparator<Priority, std::ranges::less>; | |
FunctionCollectionType callbacks {}; | |
[[nodiscard]] constexpr IdentityType generate_new_identity() const noexcept { | |
if (this->callbacks.empty()) return {}; | |
const auto identities = this->callbacks | std::views::transform(Listener::identity_transformer); | |
return std::ranges::max(identities) + static_cast<IdentityType>(1); | |
} | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#pragma once | |
namespace events { | |
enum struct Priority { | |
LOWEST, LOW, | |
MEDIUM, | |
HIGH, HIGHEST, | |
DEFAULT = MEDIUM | |
}; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <catch2/catch_test_macros.hpp> | |
#include <array> | |
#pragma clang diagnostic push | |
#pragma clang diagnostic ignored "-Wkeyword-macro" | |
#define private public | |
#include <events/listener.hpp> | |
#pragma clang diagnostic pop | |
struct DummyEvent: events::Event { | |
unsigned data {}; | |
static void first_callback(const DummyEvent &) noexcept {} | |
static constexpr auto expected_modified_data_value {42u}; | |
static void second_callback(DummyEvent &dummy_event) noexcept { | |
dummy_event.data = DummyEvent::expected_modified_data_value; | |
} | |
}; | |
static decltype(auto) create_listener() { | |
return events::Listener<DummyEvent> {}; | |
} | |
SCENARIO("Listener<Event>::add_callback accepts multiple callback types", "[listener][add_callback][types]") { | |
GIVEN("an empty event listener") { | |
auto listener = create_listener(); | |
constexpr auto expected_unmodified_data_value {0xDEADBEEF}; | |
constexpr auto expected_first_callback_identity {0uz}; | |
constexpr auto expected_first_callback_count {1uz}; | |
WHEN("inserting a void(const DummyEvent&) function") { | |
const auto callback_identity = listener.add_callback(DummyEvent::first_callback); | |
REQUIRE(callback_identity == expected_first_callback_identity); | |
REQUIRE(listener.callbacks.size() == expected_first_callback_count); | |
AND_WHEN("the listener's callbacks are invoked") { | |
constexpr auto expected_data_value {0xDEADBEEF}; | |
const auto invoke_result = listener.invoke_callbacks(expected_data_value); | |
THEN("the underlying data shouldn't change") { | |
REQUIRE(invoke_result.data == expected_data_value); | |
} | |
} | |
} | |
WHEN("inserting a void(const DummyEvent&) noexcept stateless lambda") { | |
const auto callback_identity = listener.add_callback( | |
[](const DummyEvent&) noexcept {}); | |
REQUIRE(callback_identity == expected_first_callback_identity); | |
REQUIRE(listener.callbacks.size() == expected_first_callback_count); | |
AND_WHEN("the listener's callbacks are invoked") { | |
const auto invoke_result = listener.invoke_callbacks(expected_unmodified_data_value); | |
THEN("the underlying data shouldn't change") { | |
REQUIRE(invoke_result.data == expected_unmodified_data_value); | |
} | |
} | |
} | |
WHEN("inserting a void(DummyEvent&) function") { | |
const auto callback_identity = listener.add_callback(DummyEvent::second_callback); | |
REQUIRE(callback_identity == expected_first_callback_identity); | |
REQUIRE(listener.callbacks.size() == expected_first_callback_count); | |
AND_WHEN("the listener's callbacks are invoked") { | |
const auto invoke_result = listener.invoke_callbacks(expected_unmodified_data_value); | |
THEN("the underlying data should be changed") { | |
REQUIRE(invoke_result.data == DummyEvent::expected_modified_data_value); | |
} | |
} | |
} | |
WHEN("inserting a void(DummyEvent&) noexcept stateless lambda") { | |
const auto callback_identity = listener.add_callback( | |
[](DummyEvent &dummy_event) noexcept { | |
dummy_event.data = DummyEvent::expected_modified_data_value; | |
}); | |
REQUIRE(callback_identity == expected_first_callback_identity); | |
REQUIRE(listener.callbacks.size() == expected_first_callback_count); | |
AND_WHEN("the listener's callbacks are invoked") { | |
const auto invoke_result = listener.invoke_callbacks(expected_unmodified_data_value); | |
THEN("the underlying data shouldn't change") { | |
REQUIRE(invoke_result.data == DummyEvent::expected_modified_data_value); | |
} | |
} | |
} | |
WHEN("inserting a void(DummyEvent&) noexcept stateful lambda") { | |
const auto expected_modified_data_value = DummyEvent::expected_modified_data_value; | |
const auto callback_identity = listener.add_callback( | |
[=](DummyEvent &dummy_event) noexcept { | |
dummy_event.data = expected_modified_data_value; | |
}); | |
REQUIRE(callback_identity == expected_first_callback_identity); | |
REQUIRE(listener.callbacks.size() == expected_first_callback_count); | |
AND_WHEN("the listener's callbacks are invoked") { | |
const auto invoke_result = listener.invoke_callbacks(expected_unmodified_data_value); | |
THEN("the underlying data shouldn't change") { | |
REQUIRE(invoke_result.data == expected_modified_data_value); | |
} | |
} | |
} | |
} | |
} | |
SCENARIO("Listener<Event>::add_callback inserts in priority order", "[listener][add_callback][insertion_order]") { | |
GIVEN("an event listener with multiple callbacks") { | |
auto listener = create_listener(); | |
constexpr auto callback = [](const DummyEvent &) constexpr noexcept {}; | |
using events::Priority; | |
const auto identity0 = listener.add_callback(callback, Priority::LOWEST); | |
const auto identity1 = listener.add_callback(callback); | |
const auto identity2 = listener.add_callback(callback, Priority::HIGHEST); | |
const auto identity3 = listener.add_callback(callback); | |
THEN("the amount of callbacks should be equal to 4") { | |
REQUIRE(listener.callbacks.size() == 4); | |
AND_THEN("the callbacks were inserted in a specific order") { | |
const auto expected_order = {identity2, identity1, identity3, identity0}; | |
for (std::size_t i {}; const auto identity : expected_order) | |
REQUIRE(std::get<decltype(listener)::IdentityType>(listener.callbacks[i++]) == identity); | |
} | |
} | |
} | |
} | |
SCENARIO("Listener<Event>::remove_callback erases a callback", "[listener][remove_callback]") { | |
GIVEN("an event listener with two callbacks") { | |
auto listener = create_listener(); | |
using IdentityType = decltype(listener)::IdentityType; | |
const auto first_callback_identity = listener.add_callback(DummyEvent::first_callback); | |
const auto second_callback_identity = listener.add_callback(DummyEvent::second_callback); | |
THEN("the amount of callbacks should be equal to 2") | |
REQUIRE(listener.callbacks.size() == 2); | |
WHEN("the second callback is erased") { | |
listener.remove_callback(second_callback_identity); | |
THEN("the amount of callbacks should be equal to 1") { | |
REQUIRE(listener.callbacks.size() == 1); | |
AND_THEN("the first callback entry identity should match") | |
REQUIRE(std::get<IdentityType>(listener.callbacks[0]) == first_callback_identity); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment