From 25ed8037fa04f67f3cae9f7fdf9481dc3051fb08 Mon Sep 17 00:00:00 2001 From: Amber Ehrlich Date: Thu, 19 Oct 2023 15:35:11 -0400 Subject: [PATCH 1/2] refactor(coro): deprecate dpp::job handler & push dpp::task instead, and remove static_assert in dpp::job --- include/dpp/coro.h | 3 - include/dpp/coro/async.h | 14 +++- include/dpp/coro/coroutine.h | 15 +++- include/dpp/coro/job.h | 40 +++------- include/dpp/coro/task.h | 15 +++- include/dpp/event_router.h | 148 +++++++++++++++++++++++++++++++---- include/dpp/export.h | 15 +++- include/dpp/utility.h | 5 ++ src/unittest/coro.cpp | 2 +- 9 files changed, 204 insertions(+), 53 deletions(-) diff --git a/include/dpp/coro.h b/include/dpp/coro.h index a515d0e53c..e1670e1acf 100644 --- a/include/dpp/coro.h +++ b/include/dpp/coro.h @@ -19,7 +19,6 @@ * ************************************************************************************/ -#ifdef DPP_CORO #pragma once #include "coro/async.h" @@ -27,5 +26,3 @@ #include "coro/job.h" #include "coro/task.h" #include "coro/when_any.h" - -#endif /* DPP_CORO */ diff --git a/include/dpp/coro/async.h b/include/dpp/coro/async.h index 2790aa596b..bd78369dd9 100644 --- a/include/dpp/coro/async.h +++ b/include/dpp/coro/async.h @@ -18,9 +18,19 @@ * limitations under the License. * ************************************************************************************/ +#pragma once + +#include + +namespace dpp { + +struct async_dummy { + int* dummy_shared_state = nullptr; +}; + +} #ifdef DPP_CORO -#pragma once #include "coro.h" @@ -490,6 +500,8 @@ class async : private detail::async::async_base { } }; +DPP_CHECK_ABI_COMPAT(async<>, async_dummy); + } // namespace dpp #endif /* DPP_CORO */ diff --git a/include/dpp/coro/coroutine.h b/include/dpp/coro/coroutine.h index 38b80b9127..ea656f578e 100644 --- a/include/dpp/coro/coroutine.h +++ b/include/dpp/coro/coroutine.h @@ -18,9 +18,19 @@ * limitations under the License. * ************************************************************************************/ +#pragma once + +#include + +namespace dpp { + +struct coroutine_dummy { + int *handle_dummy = nullptr; +}; + +} #ifdef DPP_CORO -#pragma once #include "coro.h" @@ -562,6 +572,9 @@ inline void coroutine::await_resume_impl() const { } #endif /* _DOXYGEN_ */ +DPP_CHECK_ABI_COMPAT(coroutine, coroutine_dummy) +DPP_CHECK_ABI_COMPAT(coroutine, coroutine_dummy) + } // namespace dpp /** diff --git a/include/dpp/coro/job.h b/include/dpp/coro/job.h index e2473f39c7..f7371977d2 100644 --- a/include/dpp/coro/job.h +++ b/include/dpp/coro/job.h @@ -18,9 +18,18 @@ * limitations under the License. * ************************************************************************************/ +#pragma once + +#include + +namespace dpp { + +struct job_dummy { +}; + +} #ifdef DPP_CORO -#pragma once #include "coro.h" @@ -49,15 +58,6 @@ namespace detail { namespace job { -template -inline constexpr bool coroutine_has_no_ref_params_v = false; - -template <> -inline constexpr bool coroutine_has_no_ref_params_v<> = true; - -template -inline constexpr bool coroutine_has_no_ref_params_v = (std::is_invocable_v || !std::is_reference_v) && (!std::is_reference_v && ... && true); - #ifdef DPP_CORO_TEST struct promise{}; #endif @@ -118,31 +118,13 @@ struct promise { * @brief Function called when the job returns. Does nothing. */ void return_void() const noexcept {} - - /** - * @brief Function that will wrap every co_await inside of the job. - */ - template - T await_transform(T &&expr) const noexcept { - /** - * `job` is extremely efficient as a coroutine but this comes with drawbacks : - * It cannot be co_awaited, which means the second it co_awaits something, the program jumps back to the calling function, which continues executing. - * At this point, if the function returns, every object declared in the function including its parameters are destroyed, which causes dangling references. - * This is exactly the same problem as references in lambdas : https://dpp.dev/lambdas-and-locals.html. - * - * If you must pass a reference, pass it as a pointer or with std::ref, but you must fully understand the reason behind this warning, and what to avoid. - * If you prefer a safer type, use `coroutine` for synchronous execution, or `task` for parallel tasks, and co_await them. - */ - static_assert(coroutine_has_no_ref_params_v, "co_await is disabled in dpp::job when taking parameters by reference. read comment above this line for more info"); - - return std::forward(expr); - } }; } // namespace job } // namespace detail +DPP_CHECK_ABI_COMPAT(job, job_dummy) } // namespace dpp /** diff --git a/include/dpp/coro/task.h b/include/dpp/coro/task.h index 5960e0b724..c208440de5 100644 --- a/include/dpp/coro/task.h +++ b/include/dpp/coro/task.h @@ -18,9 +18,19 @@ * limitations under the License. * ************************************************************************************/ +#pragma once + +#include + +namespace dpp { + +struct task_dummy { + int* handle_dummy = nullptr; +}; + +} #ifdef DPP_CORO -#pragma once #include "coro.h" @@ -747,6 +757,9 @@ inline void task::await_resume_impl() const { } #endif /* _DOXYGEN_ */ +DPP_CHECK_ABI_COMPAT(task, task_dummy) +DPP_CHECK_ABI_COMPAT(task, task_dummy) + } // namespace dpp /** diff --git a/include/dpp/event_router.h b/include/dpp/event_router.h index f759570465..038cfec168 100644 --- a/include/dpp/event_router.h +++ b/include/dpp/event_router.h @@ -35,6 +35,7 @@ #include #include #include +#include namespace dpp { @@ -200,10 +201,28 @@ template class event_router_t { private: friend class cluster; + /** @brief Non-coro event handler type */ + using regular_handler_t = std::function; + + /** @brief Type that event handlers will be stored as with DPP_CORO off. This is the ABI DPP_CORO has to match */ + using event_handler_abi_t = std::variant>; + #ifdef DPP_CORO friend class detail::event_router::awaitable; + + /** @brief dpp::task coro event handler */ + using task_handler_t = std::function(const T&)>; + + /** @brief Type that event handlers are stored as */ + using event_handler_t = std::variant; + + DPP_CHECK_ABI_COMPAT(event_handler_t, event_handler_abi_t) +#else + /** @brief Type that event handlers are stored as */ + using event_handler_t = event_handler_abi_t; #endif + /** @brief Identifier for the next event handler, will be given to the user on attaching a handler */ event_handle next_handle = 1; /** @@ -217,7 +236,7 @@ template class event_router_t { * be called in they order they are bound to the event * as std::map is an ordered container. */ - std::map> dispatch_container; + std::map dispatch_container; #ifdef DPP_CORO /** @@ -263,7 +282,57 @@ template class event_router_t { warning = warning_function; } + /** + * @brief Handle an event. This function should only be used without coro enabled, otherwise use handle_coro. + */ + void handle(const T& event) const { + if (warning) { + warning(event); + } + + std::shared_lock l(mutex); + for (const auto& [_, listener] : dispatch_container) { + if (!event.is_cancelled()) { + if (std::holds_alternative(listener)) { + std::get(listener)(event); + } else { + throw dpp::logic_exception("cannot handle a coroutine event handler with a library built without DPP_CORO"); + } + } + }; + } + #ifdef DPP_CORO + /** + * @brief Handle an event as a coroutine, ensuring the lifetime of the event object. + */ + dpp::job handle_coro(T event) const { + if (warning) { + warning(event); + } + + resume_awaiters(event); + + std::vector> tasks; + { + std::shared_lock l(mutex); + + for (const auto& [_, listener] : dispatch_container) { + if (!event.is_cancelled()) { + if (std::holds_alternative(listener)) { + tasks.push_back(std::get(listener)(event)); + } else if (std::holds_alternative(listener)) { + std::get(listener)(event); + } + } + }; + } + + for (dpp::task& t : tasks) { + co_await t; // keep the event object alive until all tasks finished + } + } + /** * @brief Attach a suspended coroutine to this event router via detail::event_router::awaitable. * It will be resumed and detached when an event satisfying its condition completes, or it is cancelled. @@ -371,20 +440,25 @@ template class event_router_t { * @param event Class to pass as parameter to all listeners. */ void call(const T& event) const { - if (warning) { - warning(event); - } - #ifdef DPP_CORO - resume_awaiters(event); + handle_coro(event); +#else + handle(event); #endif + }; - std::shared_lock l(mutex); - for (const auto& [_, listener] : dispatch_container) { - if (!event.is_cancelled()) { - listener(event); - } - }; + /** + * @brief Call all attached listeners. + * Listeners may cancel, by calling the event.cancel method. + * + * @param event Class to pass as parameter to all listeners. + */ + void call(T&& event) const { +#ifdef DPP_CORO + handle_coro(std::move(event)); +#else + handle(std::move(event)); +#endif }; #ifdef DPP_CORO @@ -524,7 +598,7 @@ template class event_router_t { * detach the listener from the event later if necessary. */ template - requires (utility::callable_returns || utility::callable_returns) + requires (utility::callable_returns || utility::callable_returns, const T&> || utility::callable_returns) [[maybe_unused]] event_handle operator()(F&& fun) { return this->attach(std::forward(fun)); } @@ -532,18 +606,60 @@ template class event_router_t { /** * @brief Attach a callable to the event, adding a listener. * The callable should either be of the form `void(const T &)` or - * `dpp::job(T)`, where T is the event type for this event router. + * `dpp::task(const T&)`, where T is the event type for this event router. * * @param fun Callable to attach to event * @return event_handle An event handle unique to this event, used to * detach the listener from the event later if necessary. */ template - requires (utility::callable_returns || utility::callable_returns) + requires (utility::callable_returns) [[maybe_unused]] event_handle attach(F&& fun) { std::unique_lock l(mutex); event_handle h = next_handle++; - dispatch_container.emplace(h, std::forward(fun)); + dispatch_container.emplace(std::piecewise_construct, std::forward_as_tuple(h), std::forward_as_tuple(std::in_place_type_t{}, std::forward(fun))); + return h; + } + + /** + * @brief Attach a callable to the event, adding a listener. + * The callable should either be of the form `void(const T &)` or + * `dpp::task(const T&)`, where T is the event type for this event router. + * + * @param fun Callable to attach to event + * @return event_handle An event handle unique to this event, used to + * detach the listener from the event later if necessary. + */ + template + requires (utility::callable_returns, const T&>) + [[maybe_unused]] event_handle attach(F&& fun) { + assert(dpp::utility::is_coro_enabled()); + + std::unique_lock l(mutex); + event_handle h = next_handle++; + dispatch_container.emplace(std::piecewise_construct, std::forward_as_tuple(h), std::forward_as_tuple(std::in_place_type_t{}, std::forward(fun))); + return h; + } + + /** + * @brief Attach a callable to the event, adding a listener. + * The callable should either be of the form `void(const T &)` or + * `dpp::task(const T&)`, where T is the event type for this event router. + * + * @deprecated dpp::job event handlers are deprecated and will be removed in a future version, use dpp::task instead. + * @param fun Callable to attach to event + * @return event_handle An event handle unique to this event, used to + * detach the listener from the event later if necessary. + */ + template + requires (utility::callable_returns) + DPP_DEPRECATED("dpp::job event handlers are deprecated and will be removed in a future version, use dpp::task instead") + [[maybe_unused]] event_handle attach(F&& fun) { + assert(dpp::utility::is_coro_enabled()); + + std::unique_lock l(mutex); + event_handle h = next_handle++; + dispatch_container.emplace(std::piecewise_construct, std::forward_as_tuple(h), std::forward_as_tuple(std::in_place_type_t{}, std::forward(fun))); return h; } # else diff --git a/include/dpp/export.h b/include/dpp/export.h index 4672f2f78d..c8ba8f6f69 100644 --- a/include/dpp/export.h +++ b/include/dpp/export.h @@ -65,4 +65,17 @@ #define NOMINMAX #include -#endif \ No newline at end of file +#endif + +#ifdef _DOXYGEN_ + /** @brief Macro that expands to [[deprecated(reason)]] when including the library, nothing when building the library */ + #define DPP_DEPRECATED(reason) +#else /* !_DOXYGEN_ */ + #if defined(DPP_BUILD) || defined(DPP_NO_DEPRECATED) + /** @brief Macro that expands to [[deprecated(reason)]] when including the library, nothing when building the library */ + #define DPP_DEPRECATED(reason) + #else + /** @brief Macro that expands to [[deprecated(reason)]] when including the library, nothing when building the library */ + #define DPP_DEPRECATED(reason) [[deprecated(reason)]] + #endif +#endif /* _DOXYGEN_ */ diff --git a/include/dpp/utility.h b/include/dpp/utility.h index ed954058fa..1410b8599e 100644 --- a/include/dpp/utility.h +++ b/include/dpp/utility.h @@ -37,6 +37,11 @@ namespace dpp { enum sticker_format : uint8_t; +/** @brief Macro that expands to static_asserts checking sizeof and alignof are equal between two types */ +#define DPP_CHECK_ABI_COMPAT(a, b) \ +static_assert(sizeof(a) == sizeof(b), #a " and " #b " must be the same size for ABI compatibility"); \ +static_assert(alignof(a) == alignof(b), #a " and " #b " must be the same alignment for ABI compatibility"); \ + /** * @brief Utility helper functions, generally for logging, running programs, time/date manipulation, etc */ diff --git a/src/unittest/coro.cpp b/src/unittest/coro.cpp index 1e6e819cf0..c5047a134c 100644 --- a/src/unittest/coro.cpp +++ b/src/unittest/coro.cpp @@ -410,7 +410,7 @@ void coro_offline_tests() } void event_handler_test(dpp::cluster *bot) { - bot->on_message_create([](dpp::message_create_t event) -> dpp::job { + bot->on_message_create([](dpp::message_create_t event) -> dpp::task { if (event.msg.content == "coro test") { dpp::cluster *bot = event.from->creator; From a48a2c25f3f5b4374366a388efe2e156a792545c Mon Sep 17 00:00:00 2001 From: Amber Ehrlich Date: Fri, 20 Oct 2023 18:09:06 -0400 Subject: [PATCH 2/2] docs(coro): update docs to reflect changes to event routers --- .../example_code/coro_awaiting_events.cpp | 4 ++-- .../example_code/coro_expiring_buttons.cpp | 4 ++-- docpages/example_code/coro_intro.cpp | 3 +-- .../example_code/coro_simple_commands1.cpp | 4 ++-- .../example_code/coro_simple_commands2.cpp | 4 ++-- .../using_coroutines/coro_introduction.md | 2 +- include/dpp/event_router.h | 22 +++++++++---------- 7 files changed, 21 insertions(+), 22 deletions(-) diff --git a/docpages/example_code/coro_awaiting_events.cpp b/docpages/example_code/coro_awaiting_events.cpp index 3f6f63cd19..f5be22b681 100644 --- a/docpages/example_code/coro_awaiting_events.cpp +++ b/docpages/example_code/coro_awaiting_events.cpp @@ -5,7 +5,7 @@ int main() { bot.on_log(dpp::utility::cout_logger()); - bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { + bot.on_slashcommand([](const dpp::slashcommand_t& event) -> dpp::task { if (event.command.get_command_name() == "test") { // Make a message and add a button with its custom ID set to the command interaction's ID so we can identify it dpp::message m{"Test"}; @@ -32,7 +32,7 @@ int main() { } }); - bot.on_ready([&bot](const dpp::ready_t & event) { + bot.on_ready([&bot](const dpp::ready_t& event) { if (dpp::run_once()) { dpp::slashcommand command{"test", "Test awaiting for an event", bot.me.id}; diff --git a/docpages/example_code/coro_expiring_buttons.cpp b/docpages/example_code/coro_expiring_buttons.cpp index bc23678576..ed58bdadad 100644 --- a/docpages/example_code/coro_expiring_buttons.cpp +++ b/docpages/example_code/coro_expiring_buttons.cpp @@ -5,7 +5,7 @@ int main() { bot.on_log(dpp::utility::cout_logger()); - bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { + bot.on_slashcommand([](const dpp::slashcommand_t& event) -> dpp::task { if (event.command.get_command_name() == "test") { // Make a message and add a button with its custom ID set to the command interaction's ID so we can identify it dpp::message m{"Test"}; @@ -38,7 +38,7 @@ int main() { } }); - bot.on_ready([&bot](const dpp::ready_t & event) { + bot.on_ready([&bot](const dpp::ready_t& event) { if (dpp::run_once()) { dpp::slashcommand command{"test", "Test awaiting for an event", bot.me.id}; diff --git a/docpages/example_code/coro_intro.cpp b/docpages/example_code/coro_intro.cpp index d01588e1e8..c48128547e 100644 --- a/docpages/example_code/coro_intro.cpp +++ b/docpages/example_code/coro_intro.cpp @@ -6,8 +6,7 @@ int main() { bot.on_log(dpp::utility::cout_logger()); /* The event is fired when someone issues your commands */ - /* Make note of passing the event by value, this is important (explained below) */ - bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { + bot.on_slashcommand([](const dpp::slashcommand_t& event) -> dpp::task { if (event.command.get_command_name() == "file") { /* Request the image from the URL specified and co_await the response */ dpp::http_request_completion_t result = co_await event.from->creator->co_request("https://dpp.dev/DPP-Logo.png", dpp::m_get); diff --git a/docpages/example_code/coro_simple_commands1.cpp b/docpages/example_code/coro_simple_commands1.cpp index 9c14b916af..09891ea125 100644 --- a/docpages/example_code/coro_simple_commands1.cpp +++ b/docpages/example_code/coro_simple_commands1.cpp @@ -5,7 +5,7 @@ int main() { bot.on_log(dpp::utility::cout_logger()); - bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { + bot.on_slashcommand([](const dpp::slashcommand_t& event) -> dpp::task { if (event.command.get_command_name() == "addemoji") { dpp::cluster *cluster = event.from->creator; // Retrieve parameter values @@ -48,7 +48,7 @@ int main() { } }); - bot.on_ready([&bot](const dpp::ready_t & event) { + bot.on_ready([&bot](const dpp::ready_t& event) { if (dpp::run_once()) { dpp::slashcommand command("addemoji", "Add an emoji", bot.me.id); // Add file and name as required parameters diff --git a/docpages/example_code/coro_simple_commands2.cpp b/docpages/example_code/coro_simple_commands2.cpp index 96855f1b3b..ac4683eb45 100644 --- a/docpages/example_code/coro_simple_commands2.cpp +++ b/docpages/example_code/coro_simple_commands2.cpp @@ -5,7 +5,7 @@ int main() { bot.on_log(dpp::utility::cout_logger()); - bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { + bot.on_slashcommand([](const dpp::slashcommand_t& event) -> dpp::task { if (event.command.get_command_name() == "avatar") { // Make a nested coroutine to fetch the guild member requested, that returns it as an optional constexpr auto resolve_member = [](const dpp::slashcommand_t &event) -> dpp::task> { @@ -70,7 +70,7 @@ int main() { }); - bot.on_ready([&bot](const dpp::ready_t & event) { + bot.on_ready([&bot](const dpp::ready_t& event) { if (dpp::run_once()) { dpp::slashcommand command("avatar", "Get your or another user's avatar image", bot.me.id); command.add_option(dpp::command_option(dpp::co_user, "user", "User to fetch the avatar from")); diff --git a/docpages/example_programs/using_coroutines/coro_introduction.md b/docpages/example_programs/using_coroutines/coro_introduction.md index 55c5e123c2..e91a4a271e 100644 --- a/docpages/example_programs/using_coroutines/coro_introduction.md +++ b/docpages/example_programs/using_coroutines/coro_introduction.md @@ -9,7 +9,7 @@ Let's revisit \ref attach-file "attaching a downloaded file", but this time with Coroutines can make commands simpler by eliminating callbacks, which can be very handy in the case of complex commands that rely on a lot of different data or steps. -In order to be a coroutine, a function has to return a special type with special functions; D++ offers dpp::job, dpp::task, and dpp::coroutine, which are designed to work seamlessly with asynchronous calls through dpp::async, which all the functions starting with `co_` such as dpp::cluster::co_message_create return. Event routers can have a dpp::job attached to them, as this object allows to create coroutines that can execute on their own, asynchronously. More on that and the difference between it and the other two types later. To turn a function into a coroutine, simply make it return dpp::job as seen in the example at line 10, then use `co_await` on awaitable types or `co_return`. The moment the execution encounters one of these two keywords, the function is transformed into a coroutine. Coroutines that use dpp::job can be used for event handlers, they can be attached to an event router just the same way as regular event handlers. +In order to be a coroutine, a function has to return a special type with special functions; D++ offers dpp::job, dpp::task, and dpp::coroutine, which are designed to work seamlessly with asynchronous calls through dpp::async, which all the functions starting with `co_` such as dpp::cluster::co_message_create return. Event routers can have a dpp::task coroutine attached to them, as this object allows to create coroutines that can execute on their own, asynchronously. More on that and the difference between it and the other two types later. To turn a function into a coroutine, simply make it return dpp::task as seen in the example at line 10, then use `co_await` on awaitable types or `co_return`. The moment the execution encounters one of these two keywords, the function is transformed into a coroutine. Coroutines that use dpp::task can be used for event handlers, they can be attached to an event router just the same way as regular event handlers. When using a `co_*` function such as `co_message_create`, the request is sent immediately and the returned dpp::async can be `co_await`-ed, at which point the coroutine suspends (pauses) and returns back to its caller; in other words, the program is free to go and do other things while the data is being retrieved and D++ will resume your coroutine when it has the data you need, which will be returned from the `co_await` expression. diff --git a/include/dpp/event_router.h b/include/dpp/event_router.h index 038cfec168..027f25a0fc 100644 --- a/include/dpp/event_router.h +++ b/include/dpp/event_router.h @@ -560,8 +560,8 @@ template class event_router_t { #ifdef _DOXYGEN_ /** * @brief Attach a callable to the event, adding a listener. - * The callable should either be of the form `void(const T &)` or - * `dpp::job(T)` (the latter requires DPP_CORO to be defined), + * The callable should either be of the form `void(const T&)` or + * `dpp::task(const T&)` (the latter requires DPP_CORO to be defined), * where T is the event type for this event router. * * This has the exact same behavior as using \ref attach(F&&) "attach". @@ -576,8 +576,8 @@ template class event_router_t { /** * @brief Attach a callable to the event, adding a listener. - * The callable should either be of the form `void(const T &)` or - * `dpp::job(T)` (the latter requires DPP_CORO to be defined), + * The callable should either be of the form `void(const T&)` or + * `dpp::task(const T&)` (the latter requires DPP_CORO to be defined), * where T is the event type for this event router. * * @param fun Callable to attach to event @@ -590,8 +590,8 @@ template class event_router_t { # ifdef DPP_CORO /** * @brief Attach a callable to the event, adding a listener. - * The callable should either be of the form `void(const T &)` or - * `dpp::job(T)`, where T is the event type for this event router. + * The callable should either be of the form `void(const T&)` or + * `dpp::task(const T&)`, where T is the event type for this event router. * * @param fun Callable to attach to event * @return event_handle An event handle unique to this event, used to @@ -605,7 +605,7 @@ template class event_router_t { /** * @brief Attach a callable to the event, adding a listener. - * The callable should either be of the form `void(const T &)` or + * The callable should either be of the form `void(const T&)` or * `dpp::task(const T&)`, where T is the event type for this event router. * * @param fun Callable to attach to event @@ -623,7 +623,7 @@ template class event_router_t { /** * @brief Attach a callable to the event, adding a listener. - * The callable should either be of the form `void(const T &)` or + * The callable should either be of the form `void(const T&)` or * `dpp::task(const T&)`, where T is the event type for this event router. * * @param fun Callable to attach to event @@ -643,7 +643,7 @@ template class event_router_t { /** * @brief Attach a callable to the event, adding a listener. - * The callable should either be of the form `void(const T &)` or + * The callable should either be of the form `void(const T&)` or * `dpp::task(const T&)`, where T is the event type for this event router. * * @deprecated dpp::job event handlers are deprecated and will be removed in a future version, use dpp::task instead. @@ -665,7 +665,7 @@ template class event_router_t { # else /** * @brief Attach a callable to the event, adding a listener. - * The callable should be of the form `void(const T &)` + * The callable should be of the form `void(const T&)` * where T is the event type for this event router. * * @param fun Callable to attach to event @@ -679,7 +679,7 @@ template class event_router_t { /** * @brief Attach a callable to the event, adding a listener. - * The callable should be of the form `void(const T &)` + * The callable should be of the form `void(const T&)` * where T is the event type for this event router. * * @warning You cannot call this within an event handler.