Skip to content

Instantly share code, notes, and snippets.

@anthonyprintup
Created October 19, 2022 10:26
Show Gist options
  • Save anthonyprintup/4f28644d5028649a77e02eb33faa4177 to your computer and use it in GitHub Desktop.
Save anthonyprintup/4f28644d5028649a77e02eb33faa4177 to your computer and use it in GitHub Desktop.
Event listener (signals) implementation.
#pragma once
namespace events {
struct Event {};
}
#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);
}
};
}
#pragma once
namespace events {
enum struct Priority {
LOWEST, LOW,
MEDIUM,
HIGH, HIGHEST,
DEFAULT = MEDIUM
};
}
#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