diff --git a/CMakeLists.txt b/CMakeLists.txt index 86d52b2..fe7c677 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.14) +cmake_minimum_required(VERSION 3.25) project(eventbus) @@ -7,7 +7,10 @@ set(CXX_STANDARD_REQUIRED ON) list(APPEND CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake) include(CPM) + option(EVENTBUS_BUILD_TESTS "Build unit tests." ON) +option(EVENTBUS_BUILD_BENCHMARKS "Build benchmarks." OFF) +option(EVENTBUS_BUILD_EXAMPLES "Build examples" OFF) # set up warnings interface project to re-use include(CompilerWarnings) @@ -23,6 +26,8 @@ if(EVENTBUS_BUILD_TESTS) CPMAddPackage( NAME doctest GITHUB_REPOSITORY onqtam/doctest + # bump CMake version to 3.5 + GIT_TAG 3a01ec37828affe4c9650004edb5b304fb9d5b75 VERSION 2.4.11 ) @@ -33,4 +38,11 @@ if(EVENTBUS_BUILD_TESTS) endif() add_subdirectory(eventbus) -add_subdirectory(demo) + +if(EVENTBUS_BUILD_EXAMPLES) + add_subdirectory(demo) +endif() + +if(EVENTBUS_BUILD_BENCHMARKS) + add_subdirectory(benchmark) +endif() diff --git a/CMakePresets.json b/CMakePresets.json index 4747b7a..1dd653b 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -1,161 +1,198 @@ { - "version": 3, - "configurePresets": [ - { - "name": "linux-base", - "hidden": true, - "description": "Target the Windows Subsystem for Linux (WSL) or a remote Linux system.", - "generator": "Ninja", - "binaryDir": "${sourceDir}/out/build/${presetName}", - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Linux" - }, - "vendor": { - "microsoft.com/VisualStudioRemoteSettings/CMake/1.0": { - "sourceDir": "$env{HOME}/.vs/$ms{projectDirName}" - } - }, - "cacheVariables": { - "EVENTBUS_BUILD_TESTS": "ON" - } - }, - { - "name": "windows-base", - "description": "Target Windows with the Visual Studio development environment.", - "hidden": true, - "generator": "Ninja", - "binaryDir": "${sourceDir}/out/build/${presetName}", - "installDir": "${sourceDir}/out/install/${presetName}", - "cacheVariables": { - "CMAKE_CXX_COMPILER": "cl", - "CMAKE_C_COMPILER": "cl" - }, - "condition": { - "type": "equals", - "lhs": "${hostSystemName}", - "rhs": "Windows" - }, - "architecture": { - "value": "x64", - "strategy": "external" - }, - "toolset": { - "value": "host=x64", - "strategy": "external" - } - }, - { - "name": "gcc-base", - "hidden": true, - "inherits": "linux-base", - "cacheVariables": { - "CMAKE_CXX_COMPILER": "g++", - "CMAKE_C_COMPILER": "gcc" - } - }, - { - "name": "gcc-debug", - "inherits": "gcc-base", - "displayName": "GCC Debug", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug" - } - }, - { - "name": "gcc-release", - "inherits": "gcc-base", - "displayName": "GCC Release", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" - } - }, - { - "name": "clang-base", - "hidden": true, - "inherits": "linux-base", - "cacheVariables": { - "CMAKE_CXX_COMPILER": "clang++", - "CMAKE_C_COMPILER": "clang" - } - }, - { - "name": "clang-debug", - "inherits": "clang-base", - "displayName": "Clang Debug", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug" - } - }, - { - "name": "clang-release", - "inherits": "clang-base", - "displayName": "Clang Release", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" - } - }, - { - "name": "clang-release-with-debug-info", - "inherits": "clang-base", - "displayName": "Clang RelWithDebInfo", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "RelWithDebInfo" - } - }, - { - "name": "x64-debug", - "displayName": "x64 Debug", - "description": "Target Windows (64-bit) with the Visual Studio development environment. (Debug)", - "inherits": "windows-base", - "architecture": { - "value": "x64", - "strategy": "external" - }, - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Debug" - } - }, - { - "name": "x64-debug-clang", - "displayName": "x64 Debug Clang", - "description": "Target Windows (64-bit) with Clang", - "inherits": "windows-base", - "cacheVariables": { - "CMAKE_C_COMPILER": "clang", - "CMAKE_CXX_COMPILER": "clang++", - "CMAKE_BUILD_TYPE": "Debug" - } - }, - { - "name": "x64-release-clang", - "displayName": "x64 Release Clang", - "description": "Target Windows (64-bit) with Clang (Release)", - "inherits": "windows-base", - "cacheVariables": { - "CMAKE_C_COMPILER": "clang", - "CMAKE_CXX_COMPILER": "clang++", - "CMAKE_BUILD_TYPE": "Release" - } - }, - { - "name": "x64-release", - "displayName": "x64 Release", - "description": "Target Windows (64-bit) with the Visual Studio development environment. (Release)", - "inherits": "x64-debug", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "Release" - } - }, - { - "name": "x64-release-with-debug", - "displayName": "x64 Release w/Debug", - "description": "Target Windows (64-bit) with the Visual Studio development environment. (RelWithDebInfo)", - "inherits": "x64-debug", - "cacheVariables": { - "CMAKE_BUILD_TYPE": "RelWithDebInfo" - } + "version": 3, + "configurePresets": [ + { + "name": "base", + "hidden": true, + "generator": "Ninja", + "binaryDir": "${sourceDir}/build/${presetName}", + "installDir": "${sourceDir}/build/install/${presetName}", + "cacheVariables": { + "CMAKE_EXPORT_COMPILE_COMMANDS": "YES", + "EVENTBUS_BUILD_TESTS": "ON", + "EVENTBUS_BUILD_EXAMPLES": "ON", + "EVENTBUS_BUILD_BENCHMARKS": "ON" + } + }, + { + "name": "macos-base", + "hidden": true, + "inherits": "base", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Darwin" + } + }, + { + "name": "clang-debug", + "inherits": "macos-base", + "displayName": "macOS Clang Debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug", + "CMAKE_CXX_COMPILER": "clang++", + "CMAKE_C_COMPILER": "clang" + } + }, + { + "name": "clang-release", + "inherits": "macos-base", + "displayName": "macOS Clang Release", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release", + "CMAKE_CXX_COMPILER": "clang++", + "CMAKE_C_COMPILER": "clang" + } + }, + { + "name": "linux-base", + "hidden": true, + "inherits": "base", + "description": "Target the Windows Subsystem for Linux (WSL) or a remote Linux system.", + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Linux" + }, + "vendor": { + "microsoft.com/VisualStudioRemoteSettings/CMake/1.0": { + "sourceDir": "$env{HOME}/.vs/$ms{projectDirName}" } - ] -} \ No newline at end of file + } + }, + { + "name": "windows-base", + "description": "Target Windows with the Visual Studio development environment.", + "hidden": true, + "inherits": "base", + "cacheVariables": { + "CMAKE_CXX_COMPILER": "cl", + "CMAKE_C_COMPILER": "cl" + }, + "condition": { + "type": "equals", + "lhs": "${hostSystemName}", + "rhs": "Windows" + }, + "architecture": { + "value": "x64", + "strategy": "external" + }, + "toolset": { + "value": "host=x64", + "strategy": "external" + } + }, + { + "name": "gcc-base", + "hidden": true, + "inherits": "linux-base", + "cacheVariables": { + "CMAKE_CXX_COMPILER": "g++", + "CMAKE_C_COMPILER": "gcc" + } + }, + { + "name": "gcc-debug", + "inherits": "gcc-base", + "displayName": "GCC Debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "gcc-release", + "inherits": "gcc-base", + "displayName": "GCC Release", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "clang-base", + "hidden": true, + "inherits": "linux-base", + "cacheVariables": { + "CMAKE_CXX_COMPILER": "clang++", + "CMAKE_C_COMPILER": "clang" + } + }, + { + "name": "clang-debug", + "inherits": "clang-base", + "displayName": "Clang Debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "clang-release", + "inherits": "clang-base", + "displayName": "Clang Release", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "clang-release-with-debug-info", + "inherits": "clang-base", + "displayName": "Clang RelWithDebInfo", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo" + } + }, + { + "name": "x64-debug", + "displayName": "x64 Debug", + "description": "Target Windows (64-bit) with the Visual Studio development environment. (Debug)", + "inherits": "windows-base", + "architecture": { + "value": "x64", + "strategy": "external" + }, + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "x64-debug-clang", + "displayName": "x64 Debug Clang", + "description": "Target Windows (64-bit) with Clang", + "inherits": "windows-base", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++", + "CMAKE_BUILD_TYPE": "Debug" + } + }, + { + "name": "x64-release-clang", + "displayName": "x64 Release Clang", + "description": "Target Windows (64-bit) with Clang (Release)", + "inherits": "windows-base", + "cacheVariables": { + "CMAKE_C_COMPILER": "clang", + "CMAKE_CXX_COMPILER": "clang++", + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "x64-release", + "displayName": "x64 Release", + "description": "Target Windows (64-bit) with the Visual Studio development environment. (Release)", + "inherits": "x64-debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "x64-release-with-debug", + "displayName": "x64 Release w/Debug", + "description": "Target Windows (64-bit) with the Visual Studio development environment. (RelWithDebInfo)", + "inherits": "x64-debug", + "cacheVariables": { + "CMAKE_BUILD_TYPE": "RelWithDebInfo" + } + } + ] +} diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt new file mode 100644 index 0000000..baf6e3f --- /dev/null +++ b/benchmark/CMakeLists.txt @@ -0,0 +1,21 @@ +cmake_minimum_required(VERSION 3.25) +project(eventbus-benchmarks) + +CPMAddPackage( + NAME nanobench + GITHUB_REPOSITORY martinus/nanobench + VERSION 4.3.11 + GIT_SHALLOW +) + +file(GLOB benchmark_sources CONFIGURE_DEPENDS src/*.cpp) +add_executable(${PROJECT_NAME} ${benchmark_sources}) +target_link_libraries( + ${PROJECT_NAME} + PUBLIC + dp::eventbus + doctest::doctest + nanobench + project_options + project_warnings +) diff --git a/benchmark/src/event_bus.cpp b/benchmark/src/event_bus.cpp new file mode 100644 index 0000000..b263d6c --- /dev/null +++ b/benchmark/src/event_bus.cpp @@ -0,0 +1,106 @@ +#include +#include + +#include +#include +#include +#include + +TEST_CASE("event dispatch - std::any vs std::variant") { + // This test compares the performance of event dispatching using std::any and std::variant + std::vector args = {1'000, 10'000, 100'000}; + + using namespace std::chrono_literals; + for (const auto& dispatch_count : args) { + ankerl::nanobench::Bench bench; + auto bench_title = + std::string("event dispatch - " + std::to_string(dispatch_count) + " times"); + bench.title(bench_title).relative(true).warmup(100).minEpochIterations(1000); + bench.timeUnit(1us, "us"); + + struct event { + int data_int; + float data_float; + double data_double; + std::uint64_t data_int_large; + }; + + struct event2 { + int data_int; + }; + + struct allocating_event { + std::vector data; + allocating_event() : data(8, std::byte{0}) {} // 8 bytes + }; + + bench.run("std::any", [dispatch_count]() { + dp::event_bus bus{}; + + auto registration1 = bus.register_handler([] {}); + auto registration2 = bus.register_handler([] {}); + ankerl::nanobench::doNotOptimizeAway(registration1); + ankerl::nanobench::doNotOptimizeAway(registration2); + + for (std::size_t i = 0; i < dispatch_count; ++i) { + bus.fire_event(event{}); + bus.fire_event(event2{}); + } + }); + + bench.run("std::variant", [dispatch_count]() { + dp::event_bus bus{}; + + auto registration1 = bus.register_handler([] {}); + auto registration2 = bus.register_handler([] {}); + ankerl::nanobench::doNotOptimizeAway(registration1); + ankerl::nanobench::doNotOptimizeAway(registration2); + + for (std::size_t i = 0; i < dispatch_count; ++i) { + bus.fire_event(event{}); + bus.fire_event(event2{}); + } + }); + } +} + +TEST_CASE("event dispatch - std::any vs std::variant with allocating event") { + // This test compares the performance of event dispatching using std::any and std::variant + std::vector args = {1'000, 10'000}; + + using namespace std::chrono_literals; + for (const auto& dispatch_count : args) { + ankerl::nanobench::Bench bench; + auto bench_title = + std::string("event dispatch - " + std::to_string(dispatch_count) + " times"); + bench.title(bench_title).relative(true).warmup(100).minEpochIterations(1000); + bench.timeUnit(1us, "us"); + + struct allocating_event { + std::vector data; + allocating_event() : data(8, std::byte{0}) {} // 8 bytes + }; + + bench.run("std::any", [dispatch_count]() { + dp::event_bus bus{}; + + auto registration1 = bus.register_handler([] {}); + ankerl::nanobench::doNotOptimizeAway(registration1); + + for (std::size_t i = 0; i < dispatch_count; ++i) { + bus.fire_event(allocating_event{}); + } + }); + + bench.run("std::variant", [dispatch_count]() { + dp::event_bus bus{}; + + auto registration1 = bus.register_handler([] {}); + ankerl::nanobench::doNotOptimizeAway(registration1); + + for (std::size_t i = 0; i < dispatch_count; ++i) { + bus.fire_event(allocating_event{}); + } + }); + } +} diff --git a/benchmark/src/main.cpp b/benchmark/src/main.cpp new file mode 100644 index 0000000..0a3f254 --- /dev/null +++ b/benchmark/src/main.cpp @@ -0,0 +1,2 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include diff --git a/cmake/CompilerWarnings.cmake b/cmake/CompilerWarnings.cmake index 02d1d35..3bf59f5 100644 --- a/cmake/CompilerWarnings.cmake +++ b/cmake/CompilerWarnings.cmake @@ -78,11 +78,15 @@ function(set_project_warnings project_name) -Wuseless-cast # warn if you perform a cast to the same type ) + message("Using compiler: ${CMAKE_CXX_COMPILER_ID}") # clang can be used with visual studio directly and uses the cl like interface. if(MSVC AND NOT ${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") set(PROJECT_WARNINGS ${MSVC_WARNINGS}) - elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang") + elseif(${CMAKE_CXX_COMPILER_ID} STREQUAL "Clang") set(PROJECT_WARNINGS ${CLANG_WARNINGS}) + elseif(${CMAKE_CXX_COMPILER_ID} STREQUAL "AppleClang") + list(REMOVE_ITEM ${CLANG_WARNINGS} -Wduplicated-cond) + set(PROJECT_WARNINGS ${CLANG_WARNINGS}) # AppleClang doesn't support -Wpedantic else() set(PROJECT_WARNINGS ${GCC_WARNINGS}) endif() diff --git a/demo/main.cpp b/demo/main.cpp index e2f1ce8..7d6f795 100644 --- a/demo/main.cpp +++ b/demo/main.cpp @@ -41,7 +41,8 @@ class internal_registration_class { dp::handler_registration reg; public: - internal_registration_class(dp::event_bus& bus) + /// CTAD not allowed in non-static struct member so we have to include the empty brackets + internal_registration_class(dp::event_bus<>& bus) : reg(std::move(bus.register_handler([](const first_event& evt) { std::cout << "test class: " << evt.message << "\n"; }))) {} diff --git a/eventbus/CMakeLists.txt b/eventbus/CMakeLists.txt index cf55380..b7148b4 100644 --- a/eventbus/CMakeLists.txt +++ b/eventbus/CMakeLists.txt @@ -4,6 +4,8 @@ project(eventbus) set(project_headers include/eventbus/detail/function_traits.hpp + include/eventbus/detail/storage_policy.hpp + include/eventbus/detail/value_traits.hpp include/eventbus/event_bus.hpp ) @@ -22,7 +24,7 @@ target_link_libraries(${PROJECT_NAME} INTERFACE project_options Threads::Threads if(EVENTBUS_BUILD_TESTS) file(GLOB_RECURSE project_test_sources CONFIGURE_DEPENDS test/*.cpp) - set(project_test_name ${PROJECT_NAME}.tests) + set(project_test_name ${PROJECT_NAME}-tests) add_executable(${project_test_name} ${project_test_sources}) target_link_libraries(${project_test_name} PUBLIC diff --git a/eventbus/include/eventbus/detail/storage_policy.hpp b/eventbus/include/eventbus/detail/storage_policy.hpp new file mode 100644 index 0000000..ba46ab7 --- /dev/null +++ b/eventbus/include/eventbus/detail/storage_policy.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include + +namespace dp::detail { + struct any_event_bus_storage_policy { + using event_type = std::any; + using event_handler = std::function; + }; + + template + struct variant_event_bus_storage_policy { + using event_type = std::variant...>; + using event_handler = std::function; + }; +} // namespace dp::detail diff --git a/eventbus/include/eventbus/detail/value_traits.hpp b/eventbus/include/eventbus/detail/value_traits.hpp new file mode 100644 index 0000000..8d641ee --- /dev/null +++ b/eventbus/include/eventbus/detail/value_traits.hpp @@ -0,0 +1,12 @@ +#pragma once + +#include +#include + +namespace dp::detail { + template + struct is_any : std::false_type {}; + template <> + struct is_any : std::true_type {}; + +} // namespace dp::detail diff --git a/eventbus/include/eventbus/event_bus.hpp b/eventbus/include/eventbus/event_bus.hpp index eb947d8..5f59dcc 100644 --- a/eventbus/include/eventbus/event_bus.hpp +++ b/eventbus/include/eventbus/event_bus.hpp @@ -10,55 +10,98 @@ #include #include #include - -#include "detail/function_traits.hpp" - +#include + +#include "eventbus/detail/function_traits.hpp" +#include "eventbus/detail/storage_policy.hpp" +#include "eventbus/detail/value_traits.hpp" + +namespace { + template + auto access_event_value(Event&& value) -> std::reference_wrapper { + if constexpr (dp::detail::is_any::value) { + return std::any_cast>(value); + } else { + return std::get>(value); + } + } +} // namespace namespace dp { - class event_bus; + struct default_event_bus_storage_policy : detail::any_event_bus_storage_policy {}; /** - * @brief A registration handle for a particular handler of an event type. - * @details This class is move constructible only. It also assumed that the lifespan of this - * object will be as long or shorter than that of the event bus. This class is move - * constructible for that reason, but there are still some cases where you can run into life - * time issues. + * @brief A central event handler class that connects event handlers with the events. */ - class handler_registration { - const void* handle_{nullptr}; - dp::event_bus* event_bus_{nullptr}; - + template + class event_bus { public: - handler_registration(const handler_registration& other) = delete; - handler_registration(handler_registration&& other) noexcept; - handler_registration& operator=(const handler_registration& other) = delete; - handler_registration& operator=(handler_registration&& other) noexcept; - ~handler_registration() noexcept; - /** - * @brief Pointer to the underlying handle. + * @brief A registration handle for a particular handler of an event type. + * @details This class is move constructible only. It also assumed that the lifespan of this + * object will be as long or shorter than that of the event bus. This class is move + * constructible for that reason, but there are still some cases where you can run into life + * time issues. */ - [[nodiscard]] const void* handle() const noexcept; + class handler_registration { + const void* handle_{nullptr}; + dp::event_bus* event_bus_{nullptr}; + + public: + handler_registration(handler_registration&& other) noexcept + : handle_(std::exchange(other.handle_, nullptr)), + event_bus_(std::exchange(other.event_bus_, nullptr)) {} + + handler_registration& operator=(handler_registration&& other) noexcept { + if (this == &other) { + return *this; + } - /** - * @brief Unregister this handler from the event bus. - */ - void unregister() noexcept; + handle_ = std::exchange(other.handle_, nullptr); + event_bus_ = std::exchange(other.event_bus_, nullptr); + return *this; + } + ~handler_registration() noexcept { unregister(); } + + handler_registration(const handler_registration& other) = delete; + handler_registration& operator=(const handler_registration& other) = delete; + /** + * @brief Pointer to the underlying handle. + */ + [[nodiscard]] const void* handle() const noexcept { return handle_; } + + /** + * @brief Unregister this handler from the event bus. + */ + void unregister() noexcept { + if (event_bus_ && handle_) { + event_bus_->remove_handler(*this); + handle_ = nullptr; + } + } - protected: - handler_registration(const void* handle, dp::event_bus* bus) noexcept; - friend class event_bus; - }; + protected: + handler_registration(const void* handle, dp::event_bus* bus) noexcept + : handle_(handle), event_bus_(bus) {} + friend class event_bus; + }; + + /// @brief Public type aliases + /// @{ + using StoragePolicy = + std::conditional_t>; + using event_type = typename StoragePolicy::event_type; + using event_handler = typename StoragePolicy::event_handler; + using handler_registration = typename event_bus::handler_registration; + /// @} + + event_bus() = default; - /** - * @brief A central event handler class that connects event handlers with the events. - */ - class event_bus { - public: /** * @brief Register an event handler for a given event type. * @tparam EventHandler The invocable event handler type. - * @param handler A callable handler of the event type. This invocation is designed for when - * `handler` takes the EventType as an argument. + * @param handler A callable handler of the event type. This invocation is designed for + * when `handler` takes the EventType as an argument. * @return A handler_registration instance for the given handler. */ template @@ -93,8 +136,8 @@ namespace dp { * @tparam ClassType Event handler class * @tparam MemberFunction Event handler member function * @param class_instance Instance of ClassType that will handle the event. - * @param function Pointer to the MemberFunction of the ClassType. This invocation is for - * when `function` takes the EventType as an argument. + * @param function Pointer to the MemberFunction of the ClassType. This invocation is + * for when `function` takes the EventType as an argument. * @return A handler_registration instance for the given handler. */ template @@ -103,9 +146,9 @@ namespace dp { using EventType = typename detail::function_traits::template arg<0>::type; - static_assert( - std::is_invocable_v, - "EventHandler must be a member function of ClassType and one EventType argument."); + static_assert(std::is_invocable_v, + "EventHandler must be a member function of ClassType and one " + "EventType argument."); return register_handler_impl( [class_instance, func = std::forward(function)]( @@ -118,8 +161,8 @@ namespace dp { * @tparam ClassType Event handler class * @tparam MemberFunction Event handler member function * @param class_instance Instance of ClassType that will handle the event. - * @param function Pointer to the MemberFunction of the ClassType. This invocation is for - * when `function` takes no arguments but wants to be fired when EventType is fired. + * @param function Pointer to the MemberFunction of the ClassType. This invocation is + * for when `function` takes no arguments but wants to be fired when EventType is fired. * @return A handler_registration instance for the given handler. */ template @@ -198,8 +241,7 @@ namespace dp { private: using mutex_type = std::shared_mutex; mutable mutex_type registration_mutex_; - std::unordered_multimap> - handler_registrations_; + std::unordered_multimap handler_registrations_; template void safe_shared_registrations_access(Callable&& callable) noexcept { @@ -221,9 +263,9 @@ namespace dp { } } - // Helper function which drastically cleans up the template parameterization requirements of - // the users of this library. EventType is now deduced from the handler function directly - // unless it takes no arguments. + // Helper function which drastically cleans up the template parameterization + // requirements of the users of this library. EventType is now deduced from the handler + // function directly unless it takes no arguments. template [[nodiscard]] handler_registration register_handler_impl(EventHandler&& handler) noexcept { using traits = detail::function_traits; @@ -234,21 +276,25 @@ namespace dp { // check if the function takes any arguments. if constexpr (traits::arity == 0) { + // arity is 0, so we can safely call the function without any arguments. safe_unique_registrations_access([&]() { auto it = handler_registrations_.emplace( type_idx, - [func = std::forward(handler)](std::any&&) { func(); }); + [func = std::forward(handler)](event_type&&) { func(); }); handle = static_cast(&(it->second)); }); } else { + // function takes at least one argument, so we need to wrap the event in a + // std::reference_wrapper to avoid copying the event. safe_unique_registrations_access([&]() { auto it = handler_registrations_.emplace( - type_idx, [func = std::forward(handler)](std::any&& value) { + type_idx, [func = std::forward(handler)](event_type&& value) { std::reference_wrapper local_event = - std::any_cast>( - std::move(value)); + ::access_event_value( + std::forward(value)); + // Check if the event type is an rvalue reference and handle accordingly if constexpr (std::is_rvalue_reference_v) { static_assert(std::is_copy_constructible_v, "Event type must be copy constructible."); @@ -265,29 +311,9 @@ namespace dp { } }; - inline const void* handler_registration::handle() const noexcept { return handle_; } - - inline void handler_registration::unregister() noexcept { - if (event_bus_ && handle_) { - event_bus_->remove_handler(*this); - handle_ = nullptr; - } - } - - inline handler_registration::handler_registration(const void* handle, - dp::event_bus* bus) noexcept - : handle_(handle), event_bus_(bus) {} - - inline handler_registration::handler_registration(handler_registration&& other) noexcept - : handle_(std::exchange(other.handle_, nullptr)), - event_bus_(std::exchange(other.event_bus_, nullptr)) {} - - inline handler_registration& handler_registration::operator=( - handler_registration&& other) noexcept { - handle_ = std::exchange(other.handle_, nullptr); - event_bus_ = std::exchange(other.event_bus_, nullptr); - return *this; - } + /// @brief CTAD guide for dp::event_bus + template + event_bus() -> event_bus<>; - inline handler_registration::~handler_registration() noexcept { unregister(); } + using handler_registration = event_bus<>::handler_registration; } // namespace dp diff --git a/eventbus/test/event_bus_tests.cpp b/eventbus/test/event_bus_tests.cpp index 8f8e2f5..c3a17a4 100644 --- a/eventbus/test/event_bus_tests.cpp +++ b/eventbus/test/event_bus_tests.cpp @@ -4,8 +4,9 @@ #include #include #include +#include +#include #include - struct test_event_type { int id{-1}; std::string event_message; @@ -81,7 +82,8 @@ TEST_CASE("deregister while dispatching") { evt_bus.register_handler(&counter, &event_handler_counter::on_test_event); struct deregister_while_dispatch_listener { - dp::event_bus* evt_bus{nullptr}; + // CTAD not allowed in non-static struct members so we have to include the empty brackets + dp::event_bus<>* evt_bus{nullptr}; std::vector* registrations{nullptr}; void on_event(test_event_type) { if (evt_bus && registrations) { @@ -229,4 +231,102 @@ TEST_CASE("Ensure events are not unnecessarily copied") { evt_bus.fire_event(const_checker); CHECK_FALSE(event_copied); -} \ No newline at end of file +} + +TEST_CASE("event_bus_variant: multi-threaded event dispatch") { + class simple_listener { + int index_; + + public: + explicit simple_listener(int index) : index_(index) {} + void on_event(const test_event_type& evt) const { + std::cout << "simple event: " << index_ << " " << evt.event_message << "\n"; + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } + }; + + dp::event_bus evt_bus{}; + + simple_listener listener_one(1); + simple_listener listener_two(2); + + auto reg_one = evt_bus.register_handler(&listener_one, &simple_listener::on_event); + auto reg_two = evt_bus.register_handler(&listener_two, &simple_listener::on_event); + + event_handler_counter event_counter; + auto event_handler_reg = evt_bus.register_handler( + &event_counter, &event_handler_counter::on_test_event); + + auto thread_one = std::thread([&evt_bus, &listener_one]() { + for (auto i = 0; i < 5; ++i) { + evt_bus.fire_event(test_event_type{3, "thread_one", 1.0}); + } + }); + + auto thread_two = std::thread([&evt_bus, &listener_two]() { + for (auto i = 0; i < 5; ++i) { + evt_bus.fire_event(test_event_type{3, "thread_two", 2.0}); + } + }); + + thread_one.join(); + thread_two.join(); + + // include the event counter + CHECK_EQ(evt_bus.handler_count(), 3); + + CHECK_EQ(event_counter.get_count(), 10); +} + +TEST_CASE("event_bus_variant: basic multi-event support") { + struct event1 { + int id; + std::string message; + }; + struct event2 { + double value; + }; + struct event3 { + char character; + }; + + dp::event_bus evt_bus{}; + event_handler_counter event_counter; + auto event_handler_reg = + evt_bus.register_handler(&event_counter, &event_handler_counter::on_test_event); + auto event_handler_reg2 = + evt_bus.register_handler(&event_counter, &event_handler_counter::on_test_event); + auto event_handler_reg3 = + evt_bus.register_handler(&event_counter, &event_handler_counter::on_test_event); + + struct conglomerate_handler { + void ev1(const event1& evt) { e1 = evt; } + void ev2(const event2& evt) { e2 = evt; } + void ev3(const event3& evt) { e3 = evt; } + + std::optional e1; + std::optional e2; + std::optional e3; + + auto combine() -> std::string { + REQUIRE(e1.has_value()); + REQUIRE(e2.has_value()); + REQUIRE(e3.has_value()); + std::stringstream oss; + oss << e1->id << " " << e1->message << " | " << e2->value << " | " << e3->character; + return oss.str(); + } + }; + + conglomerate_handler handler; + auto registration = evt_bus.register_handler(&handler, &conglomerate_handler::ev1); + auto registration2 = evt_bus.register_handler(&handler, &conglomerate_handler::ev2); + auto registration3 = evt_bus.register_handler(&handler, &conglomerate_handler::ev3); + + evt_bus.fire_event(event1{1, "Hello"}); + evt_bus.fire_event(event2{3.14}); + evt_bus.fire_event(event3{'A'}); + + CHECK_EQ(handler.combine(), "1 Hello | 3.14 | A"); + CHECK_EQ(event_counter.get_count(), 3); +}