From 392fb944c8e33ea8f6a8a95fb08f4977739b3825 Mon Sep 17 00:00:00 2001 From: Haithem Ben Ghorbal Date: Tue, 6 Oct 2026 13:04:48 +0200 Subject: [PATCH] Added dispatchIfAny and directDispatchIfAny to report whether any listener was invoked They are same as dispatch and directDispatch but return true if there is any listener for the event (for HeterEventDispatcher, any listener with the prototype matching the arguments), false if there is no listener or the dispatching is blocked by a mixin. This avoids calling hasAnyListener before dispatch, which looks up the event twice and is not atomic in multi threading. dispatch and directDispatch keep returning void and now forward to the new functions, so existing code is not affected. Added HeterCallbackList::invokeIfAny for the heter dispatcher. Added unit tests and documents. --- doc/callbacklist.md | 8 + doc/cn/callbacklist.md | 9 + doc/cn/eventdispatcher.md | 13 + doc/eventdispatcher.md | 11 + doc/hetercallbacklist.md | 9 + doc/hetereventdispatcher.md | 9 + include/eventpp/callbacklist.h | 28 +- include/eventpp/eventdispatcher.h | 64 +++- include/eventpp/hetercallbacklist.h | 10 + include/eventpp/hetereventdispatcher.h | 39 ++- tests/unittest/CMakeLists.txt | 1 + .../test_dispatcher_dispatchifany.cpp | 305 ++++++++++++++++++ tests/unittest/test_no_extra_copy_move.cpp | 41 +++ 13 files changed, 522 insertions(+), 25 deletions(-) create mode 100644 tests/unittest/test_dispatcher_dispatchifany.cpp diff --git a/doc/callbacklist.md b/doc/callbacklist.md index 47ae4b2..7627226 100644 --- a/doc/callbacklist.md +++ b/doc/callbacklist.md @@ -166,6 +166,14 @@ Invoke each callbacks in the callback list. The callbacks are called with arguments `args`. The callbacks are called in the thread same as the callee of `operator()`. +#### invokeIfAny + +```c++ +bool invokeIfAny(Args ...args) const; +``` +Same as `operator()`, but returns `true` if any callback was invoked, `false` if the callback list is empty. +Unlike checking `empty()` before `operator()`, the result is exact in multi threading, because it's decided while the callbacks are iterated. + ## Nested callback safety 1. If a callback adds another callback to the callback list during a invoking, the new callback is guaranteed not to be triggered within the same invoking. This is guaranteed by an unsigned 64 bits integer counter. This rule will be broken is the counter is overflowed to zero in a invoking, but this rule will continue working on the subsequence invoking. diff --git a/doc/cn/callbacklist.md b/doc/cn/callbacklist.md index d2dcd6d..bc7c9f7 100644 --- a/doc/cn/callbacklist.md +++ b/doc/cn/callbacklist.md @@ -179,6 +179,15 @@ void operator() (Args ...args) const; 回调函数会被用 `args` 参数作为参数调用。 回调函数会在 `operator()` 所在的线程中调用。 +#### invokeIfAny + +```c++ +bool invokeIfAny(Args ...args) const; +``` + +与 `operator()` 相同,但在有回调函数被调用时返回 `true` ,在回调函数列表为空时返回 `false` 。 +与先调用 `empty()` 再调用 `operator()` 不同,该结果在多线程下是准确的,因为它是在遍历回调函数时确定的。 + ## 嵌套回调函数安全性 diff --git a/doc/cn/eventdispatcher.md b/doc/cn/eventdispatcher.md index e443592..7b93537 100644 --- a/doc/cn/eventdispatcher.md +++ b/doc/cn/eventdispatcher.md @@ -208,6 +208,19 @@ void dispatch(T && first, Args ...args); 这两个重载函数略有区别,具体如何使用要根据 `ArgumentPassingMode` 策略而定。详情请阅读https://github.com/wqking/eventpp/blob/master/doc/policies.md 文档。 +#### dispatchIfAny + +```cpp +bool dispatchIfAny(Args ...args); + +template +bool dispatchIfAny(T && first, Args ...args); +``` + +与 `dispatch` 相同,但在有监听器被调用时返回 `true` ;在没有监听器,或者调度被 mixin 阻止( `mixinBeforeDispatch` 返回 false ,参见 MixinFilter )时返回 `false` 。 + +当需要知道事件是否被处理时,使用该函数可以避免在 `dispatch` 之前先调用 `hasAnyListener` ,后者会在内部映射表中查找两次事件,而且在多线程下不是原子的。 + ## 嵌套监听器安全 diff --git a/doc/eventdispatcher.md b/doc/eventdispatcher.md index 4283199..afc7de3 100644 --- a/doc/eventdispatcher.md +++ b/doc/eventdispatcher.md @@ -170,6 +170,17 @@ The function is synchronous. The listeners are called in the thread same as the The two overloaded functions have similar but slightly difference. How to use them depends on the `ArgumentPassingMode` policy. Please reference the [document of policies](policies.md) for more information. +#### dispatchIfAny + +```c++ +bool dispatchIfAny(Args ...args); + +template +bool dispatchIfAny(T && first, Args ...args); +``` +Same as `dispatch`, but returns `true` if any listener was invoked, `false` if there is no listener or the dispatching is blocked by a mixin (`mixinBeforeDispatch` returns false, see [MixinFilter](mixins.md)). +It's useful to know if an event was handled without calling `hasAnyListener` before `dispatch`, which would look up the event in the internal map twice, and is not atomic in multi threading. + ## Nested listener safety 1. If a listener adds another listener of the same event to the dispatcher during a dispatching, the new listener is guaranteed not to be triggered within the same dispatching. This is guaranteed by an unsigned 64 bits integer counter. This rule will be broken is the counter is overflowed to zero in a dispatching, but this rule will continue working on the subsequence dispatching. diff --git a/doc/hetercallbacklist.md b/doc/hetercallbacklist.md index b488ff1..9508d6c 100644 --- a/doc/hetercallbacklist.md +++ b/doc/hetercallbacklist.md @@ -155,3 +155,12 @@ void operator() (Args && ...args) const; Invoke each callbacks that can be called with `Args` in the callback list. The callbacks are called with arguments `args`. The callbacks are called in the thread same as the callee of `operator()`. + +#### invokeIfAny + +```c++ +template +bool invokeIfAny(Args && ...args) const; +``` +Same as `operator()`, but returns `true` if any callback that can be called with `Args` was invoked, `false` if there is no such callback. +Note: only the callbacks matching the prototype of `Args` count. If the list has callbacks for other prototypes only, the function returns `false`. diff --git a/doc/hetereventdispatcher.md b/doc/hetereventdispatcher.md index 81ab062..d76c7d5 100644 --- a/doc/hetereventdispatcher.md +++ b/doc/hetereventdispatcher.md @@ -152,3 +152,12 @@ Dispatch an event. The event type is deducted from the arguments of `dispatch`. Invoke each callbacks that can be called with `Args` in the callback list. The listeners are called with arguments `args`. The function is synchronous. The listeners are called in the thread same as the caller of `dispatch`. + +#### dispatchIfAny + +```c++ +template +bool dispatchIfAny(T && first, Args && ...args) const +``` +Same as `dispatch`, but returns `true` if any listener that can be called with `Args` was invoked, `false` if there is no such listener or the dispatching is blocked by a mixin (`mixinBeforeDispatch` returns false). +Note: only the listeners matching the prototype of `Args` count. If the event has listeners for other prototypes only, the function returns `false`. diff --git a/include/eventpp/callbacklist.h b/include/eventpp/callbacklist.h index 71267bb..61188ee 100644 --- a/include/eventpp/callbacklist.h +++ b/include/eventpp/callbacklist.h @@ -275,18 +275,35 @@ class CallbackListBase< }); } -#if !defined(__GNUC__) || __GNUC__ >= 5 void operator() (Args ...args) const { - forEachIf([&args...](Callback & callback) -> bool { + (void)doInvoke(args...); + } + + // Same as operator(), but returns true if any callback was invoked, false if the list is empty. + bool invokeIfAny(Args ...args) const + { + return doInvoke(args...); + } + +private: +#if !defined(__GNUC__) || __GNUC__ >= 5 + bool doInvoke(Args & ...args) const + { + bool invoked = false; + + forEachIf([&args..., &invoked](Callback & callback) -> bool { // We can't use std::forward here, because if we use std::forward, // for arg that is passed by value, and the callback prototype accepts it by value, // std::forward will move it and may cause the original value invalid. // That happens on any value-to-value passing, no matter the callback moves it or not. + invoked = true; callback(args...); return CanContinueInvoking::canContinueInvoking(args...); }); + + return invoked; } #else // This is a patch version for GCC 4. It inlines the unrolled doForEachIf. @@ -294,8 +311,9 @@ class CallbackListBase< // https://github.com/wqking/eventpp/issues/19 // This is a compromised patch for GCC 4, it may be not maintained or updated unless there are bugs. // We don't use the patch as main code because the patch generates longer code, and duplicated with doForEachIf. - void operator() (Args ...args) const + bool doInvoke(Args & ...args) const { + bool invoked = false; NodePtr node; { @@ -307,6 +325,7 @@ class CallbackListBase< while(node) { if(node->counter != removedCounter && counter >= node->counter) { + invoked = true; node->callback(args...); if(! CanContinueInvoking::canContinueInvoking(args...)) { break; @@ -318,10 +337,11 @@ class CallbackListBase< node = node->next; } } + + return invoked; } #endif -private: template bool doForEachIf(F && f) const { diff --git a/include/eventpp/eventdispatcher.h b/include/eventpp/eventdispatcher.h index 49c6712..9eb71ef 100644 --- a/include/eventpp/eventdispatcher.h +++ b/include/eventpp/eventdispatcher.h @@ -202,48 +202,88 @@ class EventDispatcherBase < } void dispatch(Args ...args) const + { + (void)doDispatch(args...); + } + + template + void dispatch(T && first, Args ...args) const + { + (void)doDispatch(std::forward(first), args...); + } + + // Same as dispatch, but returns true if any listener was invoked. + // Returns false if there is no listener, or the dispatching is blocked by mixins (mixinBeforeDispatch). + // Useful to avoid calling hasAnyListener before dispatch, which looks up the event twice. + bool dispatchIfAny(Args ...args) const + { + return doDispatch(args...); + } + + template + bool dispatchIfAny(T && first, Args ...args) const + { + return doDispatch(std::forward(first), args...); + } + + // Bypass any getEvent policy. The first argument is the event type. + // Most used for internal purpose. + void directDispatch(const Event & e, Args ...args) const + { + (void)doDirectDispatch(e, args...); + } + + // Same as directDispatch, but returns true if any listener was invoked. + bool directDispatchIfAny(const Event & e, Args ...args) const + { + return doDirectDispatch(e, args...); + } + +protected: + // The arguments are passed by reference to the public functions' by value arguments, + // so they are moved only once, in doDirectDispatch. + bool doDispatch(Args & ...args) const { static_assert(ArgumentPassingMode::canIncludeEventType, "Dispatching arguments count doesn't match required (Event type should be included)."); using GetEvent = typename SelectGetEvent::value>::Type; // can't std::forward(args) in GetEvent::getEvent because the pass by value arguments will be moved to getEvent - // then the other std::forward(args) to directDispatch will get empty values. - directDispatch( + // then the other std::forward(args) to invokeIfAny will get empty values. + return doDirectDispatch( GetEvent::getEvent(args...), - std::forward(args)... + args... ); } template - void dispatch(T && first, Args ...args) const + bool doDispatch(T && first, Args & ...args) const { static_assert(ArgumentPassingMode::canExcludeEventType, "Dispatching arguments count doesn't match required (Event type should NOT be included)."); using GetEvent = typename SelectGetEvent::value>::Type; - directDispatch( + return doDirectDispatch( GetEvent::getEvent(std::forward(first), args...), - std::forward(args)... + args... ); } - // Bypass any getEvent policy. The first argument is the event type. - // Most used for internal purpose. - void directDispatch(const Event & e, Args ...args) const + bool doDirectDispatch(const Event & e, Args & ...args) const { if(! internal_::ForEachMixins::forEach( this, typename std::add_lvalue_reference::type(args)...)) { - return; + return false; } const CallbackList_ * callableList = doFindCallableList(e); if(callableList) { - (*callableList)(std::forward(args)...); + return callableList->invokeIfAny(std::forward(args)...); } + + return false; } -protected: const CallbackList_ * doFindCallableList(const Event & e) const { return doFindCallableListHelper(this, e); diff --git a/include/eventpp/hetercallbacklist.h b/include/eventpp/hetercallbacklist.h index 4f7a1b0..800e2c4 100644 --- a/include/eventpp/hetercallbacklist.h +++ b/include/eventpp/hetercallbacklist.h @@ -255,6 +255,16 @@ class HeterCallbackListBase (*callbackList)(std::forward(args)...); } + // Same as operator(), but returns true if any callback of the prototype matching the arguments was invoked. + template + bool invokeIfAny(Args && ...args) const + { + using PrototypeInfo = FindPrototypeByArgs; + static_assert(PrototypeInfo::index >= 0, "Can't find invoker for the given argument types."); + + return doGetCallbackList()->invokeIfAny(std::forward(args)...); + } + private: template auto doForEachInvoke(Func && func, const H & handle, CL && callback) const diff --git a/include/eventpp/hetereventdispatcher.h b/include/eventpp/hetereventdispatcher.h index c803dd5..434387f 100644 --- a/include/eventpp/hetereventdispatcher.h +++ b/include/eventpp/hetereventdispatcher.h @@ -180,63 +180,84 @@ class HeterEventDispatcherBase template void dispatch(T && first, Args && ...args) const { - doDispatch(std::forward(first), std::forward(args)...); + (void)doDispatch(std::forward(first), std::forward(args)...); + } + + // Same as dispatch, but returns true if any listener with the prototype matching the arguments was invoked. + // Returns false if there is no listener, or the dispatching is blocked by mixins (mixinBeforeDispatch). + template + bool dispatchIfAny(T && first, Args && ...args) const + { + return doDispatch(std::forward(first), std::forward(args)...); } // Bypass any getEvent policy. The first argument is the event type. // Most used for internal purpose. template void directDispatch(const Event & e, Args && ...args) const + { + (void)directDispatchIfAny(e, std::forward(args)...); + } + + // Same as directDispatch, but returns true if any listener with the prototype matching the arguments was invoked. + template + bool directDispatchIfAny(const Event & e, Args && ...args) const { if(! internal_::ForEachMixins::forEach( this, typename std::add_lvalue_reference::type(args)...)) { - return; + return false; } const CallbackList_ * callableList = doFindCallableList(e); if(callableList) { - (*callableList)(std::forward(args)...); + return callableList->invokeIfAny(std::forward(args)...); } + + return false; } protected: template auto doDispatch(T && first, Args && ...args) const - -> typename std::enable_if::value>::type + -> typename std::enable_if::value, bool>::type { if(! internal_::ForEachMixins::forEach( this, typename std::add_lvalue_reference::type(first), typename std::add_lvalue_reference::type(args)...) ) { - return; + return false; } using GetEvent = typename SelectGetEvent::value>::Type; const auto e = GetEvent::getEvent(std::forward(first), args...); const CallbackList_ * callableList = doFindCallableList(e); if(callableList) { - (*callableList)(std::forward(first), std::forward(args)...); + return callableList->invokeIfAny(std::forward(first), std::forward(args)...); } + + return false; } template auto doDispatch(T && first, Args && ...args) const - -> typename std::enable_if::value>::type + -> typename std::enable_if::value, bool>::type { if(! internal_::ForEachMixins::forEach( this, typename std::add_lvalue_reference::type(args)...) ) { - return; + return false; } using GetEvent = typename SelectGetEvent::value>::Type; const auto e = GetEvent::getEvent(std::forward(first), args...); const CallbackList_ * callableList = doFindCallableList(e); if(callableList) { - (*callableList)(std::forward(args)...); + return callableList->invokeIfAny(std::forward(args)...); } + + return false; } const CallbackList_ * doFindCallableList(const Event & e) const diff --git a/tests/unittest/CMakeLists.txt b/tests/unittest/CMakeLists.txt index e9f8598..4d2d9ea 100644 --- a/tests/unittest/CMakeLists.txt +++ b/tests/unittest/CMakeLists.txt @@ -8,6 +8,7 @@ set(SRC_TEST test_dispatcher_basic.cpp test_dispatcher_ctors.cpp test_dispatcher_multithread.cpp + test_dispatcher_dispatchifany.cpp test_queue_basic.cpp test_queue_ctors.cpp test_queue_multithread.cpp diff --git a/tests/unittest/test_dispatcher_dispatchifany.cpp b/tests/unittest/test_dispatcher_dispatchifany.cpp new file mode 100644 index 0000000..af74487 --- /dev/null +++ b/tests/unittest/test_dispatcher_dispatchifany.cpp @@ -0,0 +1,305 @@ +// eventpp library +// Copyright (C) 2018 Wang Qi (wqking) +// Github: https://github.com/wqking/eventpp +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "test.h" +#include "eventpp/callbacklist.h" +#include "eventpp/hetercallbacklist.h" +#include "eventpp/eventdispatcher.h" +#include "eventpp/eventqueue.h" +#include "eventpp/hetereventdispatcher.h" +#include "eventpp/mixins/mixinfilter.h" + +TEST_CASE("CallbackList, invokeIfAny") +{ + eventpp::CallbackList callbackList; + + std::vector dataList(2); + + REQUIRE(! callbackList.invokeIfAny(3)); + REQUIRE(dataList == std::vector{ 0, 0 }); + + auto handle1 = callbackList.append([&dataList](const int e) { + dataList[0] += e; + }); + auto handle2 = callbackList.append([&dataList](const int e) { + dataList[1] += e; + }); + + REQUIRE(callbackList.invokeIfAny(3)); + REQUIRE(dataList == std::vector{ 3, 3 }); + + callbackList.remove(handle1); + REQUIRE(callbackList.invokeIfAny(5)); + REQUIRE(dataList == std::vector{ 3, 8 }); + + // The list is empty after removing all callbacks + callbackList.remove(handle2); + REQUIRE(! callbackList.invokeIfAny(7)); + REQUIRE(dataList == std::vector{ 3, 8 }); +} + +struct StopAfterFirstCallbackPolicies +{ + static bool canContinueInvoking(int) { + return false; + } +}; + +TEST_CASE("CallbackList, invokeIfAny, canContinueInvoking stops after the first callback") +{ + eventpp::CallbackList callbackList; + + std::vector dataList(2); + + callbackList.append([&dataList](const int e) { + dataList[0] += e; + }); + callbackList.append([&dataList](const int e) { + dataList[1] += e; + }); + + // The first callback was invoked, so the result is true even though the second one was skipped + REQUIRE(callbackList.invokeIfAny(3)); + REQUIRE(dataList == std::vector{ 3, 0 }); +} + +TEST_CASE("HeterCallbackList, invokeIfAny") +{ + eventpp::HeterCallbackList > callbackList; + + std::vector dataList(2); + + REQUIRE(! callbackList.invokeIfAny(3)); + REQUIRE(! callbackList.invokeIfAny(3, 5)); + + callbackList.append([&dataList](const int e) { + dataList[0] += e; + }); + + REQUIRE(callbackList.invokeIfAny(3)); + REQUIRE(dataList == std::vector{ 3, 0 }); + + // Only the callbacks matching the prototype count + REQUIRE(! callbackList.invokeIfAny(3, 5)); + REQUIRE(dataList == std::vector{ 3, 0 }); + + callbackList.append([&dataList](const int a, const int b) { + dataList[1] += a + b; + }); + + REQUIRE(callbackList.invokeIfAny(3, 5)); + REQUIRE(dataList == std::vector{ 3, 8 }); +} + +TEST_CASE("EventDispatcher, dispatchIfAny, int, void (int)") +{ + eventpp::EventDispatcher dispatcher; + + std::vector dataList(2); + + REQUIRE(! dispatcher.dispatchIfAny(3)); + REQUIRE(! dispatcher.dispatchIfAny(5)); + + auto handle = dispatcher.appendListener(3, [&dataList](const int e) { + dataList[0] += e; + }); + dispatcher.appendListener(5, [&dataList](const int e) { + dataList[1] += e; + }); + + REQUIRE(dispatcher.dispatchIfAny(3)); + REQUIRE(dataList == std::vector{ 3, 0 }); + + REQUIRE(dispatcher.dispatchIfAny(5)); + REQUIRE(dataList == std::vector{ 3, 5 }); + + // Event 7 was never registered + REQUIRE(! dispatcher.dispatchIfAny(7)); + REQUIRE(dataList == std::vector{ 3, 5 }); + + // The callback list of 3 exists but is empty after removing the listener + dispatcher.removeListener(3, handle); + REQUIRE(! dispatcher.dispatchIfAny(3)); + REQUIRE(dataList == std::vector{ 3, 5 }); + + // dispatch still works and returns nothing + dispatcher.dispatch(5); + REQUIRE(dataList == std::vector{ 3, 10 }); +} + +TEST_CASE("EventDispatcher, dispatchIfAny, int, void (int, int), exclude event") +{ + eventpp::EventDispatcher dispatcher; + + std::vector dataList(2); + + dispatcher.appendListener(3, [&dataList](const int a, const int b) { + dataList[0] += a + b; + }); + + REQUIRE(dispatcher.dispatchIfAny(3, 1, 2)); + REQUIRE(dataList == std::vector{ 3, 0 }); + + REQUIRE(! dispatcher.dispatchIfAny(5, 1, 2)); + REQUIRE(dataList == std::vector{ 3, 0 }); +} + +TEST_CASE("EventDispatcher, dispatchIfAny, getEvent") +{ + struct MyEvent { + int type; + int param; + }; + struct EventPolicies + { + static int getEvent(const MyEvent & e) { + return e.type; + } + }; + + eventpp::EventDispatcher dispatcher; + + int param = 0; + dispatcher.appendListener(3, [¶m](const MyEvent & e) { + param = e.param; + }); + + REQUIRE(dispatcher.dispatchIfAny(MyEvent{ 3, 5 })); + REQUIRE(param == 5); + + REQUIRE(! dispatcher.dispatchIfAny(MyEvent{ 4, 8 })); + REQUIRE(param == 5); +} + +TEST_CASE("EventDispatcher, dispatchIfAny, directDispatchIfAny bypasses getEvent") +{ + struct EventPolicies + { + static int getEvent(const int e) { + return e * 10; + } + }; + + eventpp::EventDispatcher dispatcher; + + int count = 0; + dispatcher.appendListener(30, [&count](const int) { + ++count; + }); + + // getEvent maps 3 to 30 + REQUIRE(dispatcher.dispatchIfAny(3)); + REQUIRE(! dispatcher.dispatchIfAny(30)); + REQUIRE(count == 1); + + // directDispatchIfAny takes the event as is + REQUIRE(dispatcher.directDispatchIfAny(30, 0)); + REQUIRE(count == 2); + REQUIRE(! dispatcher.directDispatchIfAny(3, 0)); + REQUIRE(count == 2); +} + +TEST_CASE("EventDispatcher, dispatchIfAny, blocked by MixinFilter returns false") +{ + struct MyPolicies { + using Mixins = eventpp::MixinList; + }; + eventpp::EventDispatcher dispatcher; + + int count = 0; + dispatcher.appendListener(3, [&count](const int) { + ++count; + }); + + REQUIRE(dispatcher.dispatchIfAny(3)); + REQUIRE(count == 1); + + dispatcher.appendFilter([](const int) -> bool { + return false; + }); + + REQUIRE(! dispatcher.dispatchIfAny(3)); + REQUIRE(count == 1); +} + +TEST_CASE("EventQueue, dispatchIfAny") +{ + eventpp::EventQueue queue; + + int count = 0; + queue.appendListener(3, [&count](const int e) { + count += e; + }); + + // EventQueue inherits dispatchIfAny from EventDispatcher + REQUIRE(queue.dispatchIfAny(3)); + REQUIRE(! queue.dispatchIfAny(5)); + REQUIRE(count == 3); + + queue.enqueue(3); + queue.process(); + REQUIRE(count == 6); +} + +TEST_CASE("HeterEventDispatcher, dispatchIfAny") +{ + eventpp::HeterEventDispatcher > dispatcher; + + std::vector dataList(2); + + REQUIRE(! dispatcher.dispatchIfAny(3, 1)); + REQUIRE(! dispatcher.dispatchIfAny(3, 1, 2)); + + dispatcher.appendListener(3, [&dataList](const int e) { + dataList[0] += e; + }); + + // Only the prototype void (int) has a listener + REQUIRE(dispatcher.dispatchIfAny(3, 1)); + REQUIRE(! dispatcher.dispatchIfAny(3, 1, 2)); + REQUIRE(dataList == std::vector{ 1, 0 }); + + dispatcher.appendListener(3, [&dataList](const int a, const int b) { + dataList[1] += a + b; + }); + + REQUIRE(dispatcher.dispatchIfAny(3, 1, 2)); + REQUIRE(dataList == std::vector{ 1, 3 }); + + REQUIRE(! dispatcher.dispatchIfAny(5, 1)); + + REQUIRE(dispatcher.directDispatchIfAny(3, 1)); + REQUIRE(! dispatcher.directDispatchIfAny(5, 1)); + REQUIRE(dataList == std::vector{ 2, 3 }); +} + +TEST_CASE("HeterEventDispatcher, dispatchIfAny, ArgumentPassingIncludeEvent") +{ + struct MyPolicies + { + using ArgumentPassingMode = eventpp::ArgumentPassingIncludeEvent; + }; + eventpp::HeterEventDispatcher, MyPolicies> dispatcher; + + int count = 0; + dispatcher.appendListener(3, [&count](const int e) { + count += e; + }); + + REQUIRE(dispatcher.dispatchIfAny(3)); + REQUIRE(count == 3); + REQUIRE(! dispatcher.dispatchIfAny(3, 1)); + REQUIRE(! dispatcher.dispatchIfAny(5)); + REQUIRE(count == 3); +} + diff --git a/tests/unittest/test_no_extra_copy_move.cpp b/tests/unittest/test_no_extra_copy_move.cpp index af57cbd..c79755d 100644 --- a/tests/unittest/test_no_extra_copy_move.cpp +++ b/tests/unittest/test_no_extra_copy_move.cpp @@ -349,3 +349,44 @@ TEST_CASE("copymove, EventDispatcher, callback(value)") } } +TEST_CASE("copymove, EventDispatcher, moved count") +{ + using ED = eventpp::EventDispatcher; + ED eventDispatcher; + + eventDispatcher.appendListener(1, [](const CopyMoveCounter & obj) { + obj.called(); + }); + + // The object variable is copied into the by value argument of dispatch, and into the by value argument + // of the std::function callback. Between them it's moved only once, into CallbackList::invokeIfAny. + SECTION("dispatch") { + CopyMoveCounter obj1; + eventDispatcher.dispatch(1, obj1); + REQUIRE(obj1.getCalledAndReset() > 0); + REQUIRE(obj1.getCounter().copied == 2); + REQUIRE(obj1.getCounter().moved == 1); + } + SECTION("dispatchIfAny") { + CopyMoveCounter obj1; + REQUIRE(eventDispatcher.dispatchIfAny(1, obj1)); + REQUIRE(obj1.getCalledAndReset() > 0); + REQUIRE(obj1.getCounter().copied == 2); + REQUIRE(obj1.getCounter().moved == 1); + } + SECTION("directDispatch") { + CopyMoveCounter obj1; + eventDispatcher.directDispatch(1, obj1); + REQUIRE(obj1.getCalledAndReset() > 0); + REQUIRE(obj1.getCounter().copied == 2); + REQUIRE(obj1.getCounter().moved == 1); + } + SECTION("directDispatchIfAny") { + CopyMoveCounter obj1; + REQUIRE(eventDispatcher.directDispatchIfAny(1, obj1)); + REQUIRE(obj1.getCalledAndReset() > 0); + REQUIRE(obj1.getCounter().copied == 2); + REQUIRE(obj1.getCounter().moved == 1); + } +} +