diff --git a/Doxyfile b/Doxyfile index eee7bad53e..18dbbc539c 100644 --- a/Doxyfile +++ b/Doxyfile @@ -278,7 +278,7 @@ OPTIMIZE_OUTPUT_VHDL = NO # Note that for custom extensions you also need to set FILE_PATTERNS otherwise # the files are not read by doxygen. -EXTENSION_MAPPING = +EXTENSION_MAPPING = dox=md # If the MARKDOWN_SUPPORT tag is enabled then doxygen pre-processes all comments # according to the Markdown format, which allows for more readable @@ -789,7 +789,8 @@ RECURSIVE = YES EXCLUDE = deps \ build \ include/dpp/format \ - include/dpp/nlohmann + include/dpp/nlohmann \ + docpages/include # The EXCLUDE_SYMLINKS tag can be used to select whether or not files or # directories that are symbolic links (a Unix file system feature) are excluded @@ -829,7 +830,7 @@ EXCLUDE_SYMBOLS = nlohmann::* \ # that contain example code fragments that are included (see the \include # command). -EXAMPLE_PATH = +EXAMPLE_PATH =docpages/include # If the value of the EXAMPLE_PATH tag contains directories, you can use the # EXAMPLE_PATTERNS tag to specify one or more wildcard pattern (like *.cpp and diff --git a/docpages/05_example_programs.md b/docpages/05_example_programs.md index 19a798271a..f44b5cb95e 100644 --- a/docpages/05_example_programs.md +++ b/docpages/05_example_programs.md @@ -5,6 +5,7 @@ There are example programs here for all skill levels demonstrating a great many * \subpage the-basics * \subpage interactions-and-components * \subpage music-and-audio +* \subpage using-coroutines * \subpage misc Is the example you are looking for missing from these sections? Pop over to our [discord server](https://discord.com/dpp) and let us know what you need... Or, even better, if you know how to do something and want to contribute an example of your own, just submit a PR! \ No newline at end of file diff --git a/docpages/06_advanced_reference.md b/docpages/06_advanced_reference.md index 8292b7cbe0..7cd4009216 100644 --- a/docpages/06_advanced_reference.md +++ b/docpages/06_advanced_reference.md @@ -5,9 +5,8 @@ * \subpage coding-standards "Coding Style Standards" * \subpage unit-tests "Unit Tests" * \subpage lambdas-and-locals "Ownership of local variables and safely transferring into a lambda" -* \subpage coroutines "Advanced commands with coroutines" * \subpage governance "Project Governance" * \subpage roadmap "Development Roadmap" * \subpage security "Security" * \subpage automating-with-jenkins "Automating your bot with Jenkins" -* \subpage separate-events "Separating events into new classes" \ No newline at end of file +* \subpage separate-events "Separating events into new classes" diff --git a/docpages/advanced_reference/coroutines.md b/docpages/advanced_reference/coroutines.md deleted file mode 100644 index b8fd5070ed..0000000000 --- a/docpages/advanced_reference/coroutines.md +++ /dev/null @@ -1,225 +0,0 @@ -\page coroutines Advanced commands with coroutines - -\warning D++ Coroutines are a very new feature and are currently only supported by D++ on g++ 11, clang/LLVM 14, and MSVC 19.37 or above. Additionally, D++ must be built with the CMake option DPP_CORO, and your program must both define the macro DPP_CORO and use C++20 or above. The feature is experimental and may have bugs or even crashes, please report any to [GitHub Issues](https://github.com/brainboxdotcc/DPP/issues) or to our [Discord Server](https://discord.gg/dpp). - -### What is a coroutine? - -Introduced in C++20, coroutines are the solution to the impracticality of callbacks. In short, a coroutine is a function that can be paused and resumed later. They are an extremely powerful alternative to callbacks for asynchronous APIs in particular, as the function can be paused when waiting for an API response, and resumed when it is received. - -Let's revisit \ref attach-file "attaching a downloaded file", but this time with a coroutine: - - -~~~~~~~~~~~~~~~{.cpp} -#include - -int main() { - dpp::cluster bot("token", dpp::i_default_intents | dpp::i_message_content); - - bot.on_log(dpp::utility::cout_logger()); - - /* Message handler to look for a command called !file */ - /* Make note of passing the event by value, this is important (explained below) */ - bot.on_message_create([](dpp::message_create_t event) -> dpp::job { - dpp::cluster *cluster = event.from->creator; - - if (event.msg.content == "!file") { - // request an image and co_await the response - dpp::http_request_completion_t result = co_await cluster->co_request("https://dpp.dev/DPP-Logo.png", dpp::m_get); - - // create a message - dpp::message msg(event.msg.channel_id, "This is my new attachment:"); - - // attach the image on success - if (result.status == 200) { - msg.add_file("logo.png", result.body); - } - - // send the message - cluster->message_create(msg); - } - }); - - bot.start(dpp::st_wait); - return 0; -} -~~~~~~~~~~~~~~~ - - -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. - -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. - -\attention You may hear that coroutines are "writing async code as if it was sync", while this is sort of correct, it may limit your understanding and especially the dangers of coroutines. I find **they are best thought of as a shortcut for a state machine**, if you've ever written one, you know what this means. Think of the lambda as *its constructor*, in which captures are variable parameters. Think of the parameters passed to your lambda as data members in your state machine. When you `co_await` something, the state machine's function exits, the program goes back to the caller, at this point the calling function may return. References are kept as references in the state machine, which means by the time the state machine is resumed, the reference may be dangling : \ref lambdas-and-locals "this is not good"! As a rule of thumb when making coroutines, **always prefer taking parameters by value and avoid lambda capture**. Another way to think of them is just like callbacks but keeping the current scope intact. In fact this is exactly what it is, the co_* functions call the normal API calls, with a callback that resumes the coroutine, *in the callback thread*. This means you cannot rely on thread_local variables and need to keep in mind concurrency issues with global states, as your coroutine will be resumed in another thread than the one it started on. - -### Several steps in one - -\note The next example assumes you are already familiar with how to use \ref firstbot "slash commands", \ref slashcommands "parameters", and \ref discord-application-command-file-upload "sending files through a command". - -Here is another example of what is made easier with coroutines, an "addemoji" command taking a file and a name as a parameter. This means downloading the emoji, submitting it to Discord, and finally replying, with some error handling along the way. Normally we would have to use callbacks and some sort of object keeping track of our state, but with coroutines, it becomes much simpler: - -~~~~~~~~~~{.cpp} -#include - -int main() { - dpp::cluster bot("token", dpp::i_default_intents | dpp::i_message_content); - - bot.on_log(dpp::utility::cout_logger()); - - bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { - if (event.command.get_command_name() == "addemoji") { - dpp::cluster *cluster = event.from->creator; - // Retrieve parameter values - dpp::snowflake file_id = std::get(event.get_parameter("file")); - std::string emoji_name = std::get(event.get_parameter("name")); - - // Get the attachment from the resolved list - const dpp::attachment &attachment = event.command.get_resolved_attachment(file_id); - - // For simplicity for this example we only support PNG - if (attachment.content_type != "image/png") { - // While event.co_reply is available, we can just use event.reply, as we will exit the command anyway and don't need to wait on the result - event.reply("Error: type " + attachment.content_type + " not supported"); - co_return; - } - // Send a " is thinking..." message, to wait on later so we can edit - dpp::async thinking = event.co_thinking(false); - - // Download and co_await the result - dpp::http_request_completion_t response = co_await cluster->co_request(attachment.url, dpp::m_get); - - if (response.status != 200) { // Page didn't send the image - co_await thinking; // Wait for the thinking response to arrive so we can edit - event.edit_response("Error: could not download the attachment"); - } - else { - // Load the image data in a dpp::emoji - dpp::emoji emoji(emoji_name); - emoji.load_image(response.body, dpp::image_type::i_png); - - // Create the emoji and co_await the response - dpp::confirmation_callback_t confirmation = co_await cluster->co_guild_emoji_create(event.command.guild_id, emoji); - - co_await thinking; // Wait for the thinking response to arrive so we can edit - if (confirmation.is_error()) - event.edit_response("Error: could not add emoji: " + confirmation.get_error().message); - else // Success - event.edit_response("Successfully added " + confirmation.get().get_mention()); // Show the new emoji - } - } - }); - - 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 - command.add_option(dpp::command_option(dpp::co_attachment, "file", "Select an image", true)); - command.add_option(dpp::command_option(dpp::co_string, "name", "Name of the emoji to add", true)); - - bot.global_command_create(command); - } - }); - - bot.start(dpp::st_wait); -} -~~~~~~~~~~ - -### I heard you liked tasks - -\note This next example is fairly advanced and makes use of many of both C++ and D++'s advanced features. - -Earlier we mentioned two other types of coroutines provided by dpp: dpp::coroutine and dpp::task. They both take their return type as a template parameter, which may be void. Both dpp::job and dpp::task start on the constructor for asynchronous execution, however only the latter can be `co_await`-ed, this allows you to retrieve its return value. If a dpp::task is destroyed before it ends, it is cancelled and will stop when it is resumed from the next `co_await`. dpp::coroutine also has a return value and can be `co_await`-ed, however it only starts when `co_await`-ing, meaning it is executed synchronously. - -Here is an example of a command making use of dpp::task to retrieve the avatar of a specified user, or if missing, the sender: - -~~~~~~~~~~{.cpp} -#include - -int main() { - dpp::cluster bot("token", dpp::i_default_intents | dpp::i_message_content); - - bot.on_log(dpp::utility::cout_logger()); - - bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { - 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> { - const dpp::command_value &user_param = event.get_parameter("user"); - dpp::snowflake user_id; - - if (std::holds_alternative(user_param)) - user_id = event.command.usr.id; // Parameter is empty so user is sender - else if (std::holds_alternative(user_param)) - user_id = std::get(user_param); // Parameter has a user - - // If we have the guild member in the command's resolved data, return it - const auto &member_map = event.command.resolved.members; - if (auto member = member_map.find(user_id); member != member_map.end()) - co_return member->second; - - // Try looking in guild cache - dpp::guild *guild = dpp::find_guild(event.command.guild_id); - if (guild) { - // Look in guild's member cache - if (auto member = guild->members.find(user_id); member != guild->members.end()) { - co_return member->second; - } - } - - // Finally if everything else failed, request API - dpp::confirmation_callback_t confirmation = co_await event.from->creator->co_guild_get_member(event.command.guild_id, user_id); - if (confirmation.is_error()) - co_return std::nullopt; // Member not found, return empty - else - co_return confirmation.get(); - }; - - // Send a " is thinking..." message, to wait on later so we can edit - dpp::async thinking = event.co_thinking(false); - - // Call our coroutine defined above to retrieve the member requested - std::optional member = co_await resolve_member(event); - - if (!member.has_value()) { - // Wait for the thinking response to arrive to make sure we can edit - co_await thinking; - event.edit_original_response(dpp::message{"User not found in this server!"}); - co_return; - } - - std::string avatar_url = member->get_avatar_url(512); - if (avatar_url.empty()) { // Member does not have a custom avatar for this server, get their user avatar - dpp::confirmation_callback_t confirmation = co_await event.from->creator->co_user_get_cached(member->user_id); - - if (confirmation.is_error()) - { - // Wait for the thinking response to arrive to make sure we can edit - co_await thinking; - event.edit_original_response(dpp::message{"User not found!"}); - co_return; - } - avatar_url = confirmation.get().get_avatar_url(512); - } - - // Wait for the thinking response to arrive to make sure we can edit - co_await thinking; - event.edit_original_response(dpp::message{avatar_url}); - } - }); - - - 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")); - - bot.global_command_create(command); - } - }); - - bot.start(dpp::st_wait); -} -~~~~~~~~~~ diff --git a/docpages/example_programs/using_coroutines.md b/docpages/example_programs/using_coroutines.md new file mode 100644 index 0000000000..681ab7b689 --- /dev/null +++ b/docpages/example_programs/using_coroutines.md @@ -0,0 +1,10 @@ +\page using-coroutines Using Coroutines + +\include{doc} coro_warn.dox + +One of the most anticipated features of C++20 is the addition of coroutines: in short, they are functions that can be paused while waiting for data and resumed when that data is ready. They make asynchronous programs much easier to write, but they do come with additional dangers and subtleties. + +* \subpage coro-introduction +* \subpage coro-simple-commands +* \subpage awaiting-events +* \subpage expiring-buttons diff --git a/docpages/example_programs/using_coroutines/awaiting_events.md b/docpages/example_programs/using_coroutines/awaiting_events.md new file mode 100644 index 0000000000..f3e28878cb --- /dev/null +++ b/docpages/example_programs/using_coroutines/awaiting_events.md @@ -0,0 +1,53 @@ +\page awaiting-events Waiting for events + +\include{doc} coro_warn.dox + +D++ makes it possible to await events: simple use `co_await` on any of the event routers, such as \ref dpp::cluster::on_message_create "on_message_create", and your coroutine will be suspended until the next event fired by this event router. You can also `co_await` the return of an event router's \ref dpp::event_router_t::when "when()" method while passing it a predicate function object, it will only resume your coroutine when the predicate returns true. Be aware that your coroutine is attached to the event router only when you call `co_await` and not before, and will be detached as it is resumed. + +\note When the event router resumes your coroutine, it will give you __a reference to the event object__. This will likely mean it will be destroyed after your next co_await, make sure to save it in a local variable if you need it for longer. + +~~~~~~~~~~cpp +#include + +int main() { + dpp::cluster bot("token", dpp::i_default_intents | dpp::i_message_content); + + bot.on_log(dpp::utility::cout_logger()); + + bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { + 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"}; + std::string id{event.command.id.str()}; + m.add_component( + dpp::component{}.add_component(dpp::component{}.set_type(dpp::cot_button).set_label("Click me!").set_id(id)) + ); + co_await event.co_reply(m); + + dpp::button_click_t click_event = co_await event.from->creator->on_button_click.when( + // Note!! Due to a bug in g++11 and g++12, id must be captured as a reference here or the compiler will destroy it twice. This is fixed in g++13 + [&id] (dpp::button_click_t const &b) { + return b.custom_id == id; + } + ); + // Acknowledge the click with an empty message and edit the original response, removing the button + click_event.reply(dpp::ir_deferred_update_message, dpp::message{}); + event.edit_original_response(dpp::message{"You clicked the button!"}); + } + }); + + 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}; + + bot.global_command_create(command); + } + }); + + bot.start(dpp::st_wait); +} +~~~~~~~~~~ + +Note that there is a problem with that! If the user never clicks your button, or if the message gets deleted, your coroutine will be stuck waiting... And waiting... Forever until your bot shuts down, occupying a space in memory. This is where the \ref expiring-buttons "next example" comes into play as a solution, with a button that expires with time. + +\image html waiting_coroutine.jpg diff --git a/docpages/example_programs/using_coroutines/coro_introduction.md b/docpages/example_programs/using_coroutines/coro_introduction.md new file mode 100644 index 0000000000..bcb5042c92 --- /dev/null +++ b/docpages/example_programs/using_coroutines/coro_introduction.md @@ -0,0 +1,54 @@ +\page coro-introduction Introduction to coroutines + +Introduced in C++20, coroutines are the solution to the impracticality of callbacks. In short, a coroutine is a function that can be paused and resumed later. They are an extremely powerful alternative to callbacks for asynchronous APIs in particular, as the function can be paused when waiting for an API response, and resumed when it is received. + +Let's revisit \ref attach-file "attaching a downloaded file", but this time with a coroutine: + + +~~~~~~~~~~~~~~~cpp +#include + +int main() { + dpp::cluster bot("token", dpp::i_default_intents | dpp::i_message_content); + + bot.on_log(dpp::utility::cout_logger()); + + /* Message handler to look for a command called !file */ + /* Make note of passing the event by value, this is important (explained below) */ + bot.on_message_create([](dpp::message_create_t event) -> dpp::job { + dpp::cluster *cluster = event.from->creator; + + if (event.msg.content == "!file") { + // request an image and co_await the response + dpp::http_request_completion_t result = co_await cluster->co_request("https://dpp.dev/DPP-Logo.png", dpp::m_get); + + // create a message + dpp::message msg(event.msg.channel_id, "This is my new attachment:"); + + // attach the image on success + if (result.status == 200) { + msg.add_file("logo.png", result.body); + } + + // send the message + cluster->message_create(msg); + } + }); + + bot.start(dpp::st_wait); + return 0; +} +~~~~~~~~~~~~~~~ + + +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. + +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. + +\warning As a rule of thumb when making coroutines, **always prefer taking parameters by value and avoid lambda capture**! See below for an explanation. + +You may hear that coroutines are "writing async code as if it was sync", while this is sort of correct, it may limit your understanding and especially the dangers of coroutines. I find **they are best thought of as a shortcut for a state machine**, if you've ever written one, you know what this means. Think of the lambda as *its constructor*, in which captures are variable parameters. Think of the parameters passed to your lambda as data members in your state machine. When you `co_await` something, the state machine's function exits, the program goes back to the caller, at this point the calling function may return. References are kept as references in the state machine, which means by the time the state machine is resumed, the reference may be dangling: \ref lambdas-and-locals "this is not good"! + +Another way to think of them is just like callbacks but keeping the current scope intact. In fact this is exactly what it is, the co_* functions call the normal API calls, with a callback that resumes the coroutine, *in the callback thread*. This means you cannot rely on thread_local variables and need to keep in mind concurrency issues with global states, as your coroutine will be resumed in another thread than the one it started on. diff --git a/docpages/example_programs/using_coroutines/coro_simple_commands.md b/docpages/example_programs/using_coroutines/coro_simple_commands.md new file mode 100644 index 0000000000..5ac09c3939 --- /dev/null +++ b/docpages/example_programs/using_coroutines/coro_simple_commands.md @@ -0,0 +1,173 @@ +\page coro-simple-commands Making simple commands + +\include{doc} coro_warn.dox + +### Several steps in one + +\note The next example assumes you are already familiar with how to use \ref firstbot "slash commands", \ref slashcommands "parameters", and \ref discord-application-command-file-upload "sending files through a command". + +With coroutines, it becomes a lot easier to do several asynchronous requests for one task. As an example an "addemoji" command taking a file and a name as a parameter. This means downloading the emoji, submitting it to Discord, and finally replying, with some error handling along the way. Normally we would have to use callbacks and some sort of object keeping track of our state, but with coroutines, the function can simply pause and be resumed when we receive the response to our request : + +~~~~~~~~~~cpp +#include + +int main() { + dpp::cluster bot("token", dpp::i_default_intents | dpp::i_message_content); + + bot.on_log(dpp::utility::cout_logger()); + + bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { + if (event.command.get_command_name() == "addemoji") { + dpp::cluster *cluster = event.from->creator; + // Retrieve parameter values + dpp::snowflake file_id = std::get(event.get_parameter("file")); + std::string emoji_name = std::get(event.get_parameter("name")); + + // Get the attachment from the resolved list + const dpp::attachment &attachment = event.command.get_resolved_attachment(file_id); + + // For simplicity for this example we only support PNG + if (attachment.content_type != "image/png") { + // While event.co_reply is available, we can just use event.reply, as we will exit the command anyway and don't need to wait on the result + event.reply("Error: type " + attachment.content_type + " not supported"); + co_return; + } + // Send a " is thinking..." message, to wait on later so we can edit + dpp::async thinking = event.co_thinking(false); + + // Download and co_await the result + dpp::http_request_completion_t response = co_await cluster->co_request(attachment.url, dpp::m_get); + + if (response.status != 200) { // Page didn't send the image + co_await thinking; // Wait for the thinking response to arrive so we can edit + event.edit_response("Error: could not download the attachment"); + } else { + // Load the image data in a dpp::emoji + dpp::emoji emoji(emoji_name); + emoji.load_image(response.body, dpp::image_type::i_png); + + // Create the emoji and co_await the response + dpp::confirmation_callback_t confirmation = co_await cluster->co_guild_emoji_create(event.command.guild_id, emoji); + + co_await thinking; // Wait for the thinking response to arrive so we can edit + if (confirmation.is_error()) { + event.edit_response("Error: could not add emoji: " + confirmation.get_error().message); + } else { // Success + event.edit_response("Successfully added " + confirmation.get().get_mention()); // Show the new emoji + } + } + } + }); + + 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 + command.add_option(dpp::command_option(dpp::co_attachment, "file", "Select an image", true)); + command.add_option(dpp::command_option(dpp::co_string, "name", "Name of the emoji to add", true)); + + bot.global_command_create(command); + } + }); + + bot.start(dpp::st_wait); +} +~~~~~~~~~~ + +### I heard you liked tasks + +\note This next example is fairly advanced and makes use of many of both C++ and D++'s advanced features. + +Earlier we mentioned two other types of coroutines provided by dpp: dpp::coroutine and dpp::task. They both take their return type as a template parameter, which may be void. Both dpp::job and dpp::task start on the constructor for asynchronous execution, however only the latter can be `co_await`-ed, this allows you to retrieve its return value. If a dpp::task is destroyed before it ends, it is cancelled and will stop when it is resumed from the next `co_await`. dpp::coroutine also has a return value and can be `co_await`-ed, however it only starts when `co_await`-ing, meaning it is executed synchronously. + +Here is an example of a command making use of dpp::task to retrieve the avatar of a specified user, or if missing, the sender: + +~~~~~~~~~~cpp +#include + +int main() { + dpp::cluster bot("token", dpp::i_default_intents | dpp::i_message_content); + + bot.on_log(dpp::utility::cout_logger()); + + bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { + 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> { + const dpp::command_value &user_param = event.get_parameter("user"); + dpp::snowflake user_id; + + if (std::holds_alternative(user_param)) + user_id = event.command.usr.id; // Parameter is empty so user is sender + else if (std::holds_alternative(user_param)) + user_id = std::get(user_param); // Parameter has a user + + // If we have the guild member in the command's resolved data, return it + const auto &member_map = event.command.resolved.members; + if (auto member = member_map.find(user_id); member != member_map.end()) + co_return member->second; + + // Try looking in guild cache + dpp::guild *guild = dpp::find_guild(event.command.guild_id); + if (guild) { + // Look in guild's member cache + if (auto member = guild->members.find(user_id); member != guild->members.end()) { + co_return member->second; + } + } + + // Finally if everything else failed, request API + dpp::confirmation_callback_t confirmation = co_await event.from->creator->co_guild_get_member(event.command.guild_id, user_id); + if (confirmation.is_error()) + co_return std::nullopt; // Member not found, return empty + else + co_return confirmation.get(); + }; + + // Send a " is thinking..." message, to wait on later so we can edit + dpp::async thinking = event.co_thinking(false); + + // Call our coroutine defined above to retrieve the member requested + std::optional member = co_await resolve_member(event); + + if (!member.has_value()) { + // Wait for the thinking response to arrive to make sure we can edit + co_await thinking; + event.edit_original_response(dpp::message{"User not found in this server!"}); + co_return; + } + + std::string avatar_url = member->get_avatar_url(512); + if (avatar_url.empty()) { // Member does not have a custom avatar for this server, get their user avatar + dpp::confirmation_callback_t confirmation = co_await event.from->creator->co_user_get_cached(member->user_id); + + if (confirmation.is_error()) { + // Wait for the thinking response to arrive to make sure we can edit + co_await thinking; + event.edit_original_response(dpp::message{"User not found!"}); + co_return; + } + avatar_url = confirmation.get().get_avatar_url(512); + } + + // Wait for the thinking response to arrive to make sure we can edit + co_await thinking; + event.edit_original_response(dpp::message{avatar_url}); + } + }); + + + 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")); + + bot.global_command_create(command); + } + }); + + bot.start(dpp::st_wait); +} +~~~~~~~~~~ diff --git a/docpages/example_programs/using_coroutines/expiring_buttons.md b/docpages/example_programs/using_coroutines/expiring_buttons.md new file mode 100644 index 0000000000..0412f84097 --- /dev/null +++ b/docpages/example_programs/using_coroutines/expiring_buttons.md @@ -0,0 +1,56 @@ +\page expiring-buttons Making expiring buttons with when_any + +\include{doc} coro_warn.dox + +In the last example we've explored how to \ref awaiting-events "await events" using coroutines, we ran into the problem of the coroutine waiting forever if the button was never clicked. Wouldn't it be nice if we could add an "or" to our algorithm, for example wait for the button to be clicked *or* for a timer to expire? I'm glad you asked! D++ offers \ref dpp::when_any "when_any" which allows exactly that. It is a templated class that can take any number of awaitable objects and can be `co_await`-ed itself, will resume when the __first__ awaitable completes and return a \ref dpp::when_any::result "result" object that allows to retrieve which awaitable completed as well as its result, in a similar way as std::variant. + +~~~~~~~~~~cpp +#include + +int main() { + dpp::cluster bot("token", dpp::i_default_intents | dpp::i_message_content); + + bot.on_log(dpp::utility::cout_logger()); + + bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { + 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"}; + std::string id{event.command.id.str()}; + m.add_component( + dpp::component{}.add_component(dpp::component{}.set_type(dpp::cot_button).set_label("Click me!").set_id(id)) + ); + co_await event.co_reply(m); + + auto result = co_await dpp::when_any{ // Whichever completes first... + event.from->creator->on_button_click.when([&id](const dpp::button_click_t &b) { return b.custom_id == id; }), // Button clicked + event.from->creator->co_sleep(5) // Or sleep 5 seconds + }; + // Note!! Due to a bug in g++11 and g++12, id must be captured as a reference above or the compiler will destroy it twice. This is fixed in g++13 + if (result.index() == 0) { // Awaitable #0 completed first, that is the button click event + // Acknowledge the click with an empty message and edit the original response, removing the button + auto &click_event = result.get<0>(); + click_event.reply(dpp::ir_deferred_update_message, dpp::message{}); + event.edit_original_response(dpp::message{"You clicked the button with the id " + click_event.custom_id}); + } + else { // Here index() is 1, the timer expired + event.edit_original_response(dpp::message{"I haven't got all day!"}); + } + } + }); + + 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}; + + bot.global_command_create(command); + } + }); + + bot.start(dpp::st_wait); +} +~~~~~~~~~~ + +Any awaitable can be used with when_any, even dpp::task, dpp::coroutine, dpp::async. When the when_any object is destroyed, any of its awaitables with a cancel() method (for example \ref dpp::task::cancel "dpp::task") will have it called. With this you can easily make commands that ask for input in several steps, or maybe a timed text game, the possibilities are endless! Note that if the first awaitable completes with an exception, result.get will throw it. + +\note when_any will try to construct awaitable objects from the parameter you pass it, which it will own. In practice this means you can only pass it temporary objects (rvalues) as most of the coroutine-related objects in D++ are move-only. diff --git a/docpages/images/waiting_coroutine.jpg b/docpages/images/waiting_coroutine.jpg new file mode 100644 index 0000000000..36ad7afc7d Binary files /dev/null and b/docpages/images/waiting_coroutine.jpg differ diff --git a/docpages/include/coro_warn.dox b/docpages/include/coro_warn.dox new file mode 100644 index 0000000000..debcb0f0dc --- /dev/null +++ b/docpages/include/coro_warn.dox @@ -0,0 +1 @@ +\warning D++ Coroutines are a very new feature and are currently only supported by D++ on g++ 11, clang/LLVM 14, and MSVC 19.37 or above. Additionally, D++ must be built with the CMake option DPP_CORO, and your program must both define the macro DPP_CORO and use C++20 or above. The feature is experimental and may have bugs or even crashes, please report any to GitHub Issues or to our Discord Server. diff --git a/include/dpp/coro/async.h b/include/dpp/coro/async.h index 2eb5f18e6b..2790aa596b 100644 --- a/include/dpp/coro/async.h +++ b/include/dpp/coro/async.h @@ -40,10 +40,12 @@ namespace detail { */ struct empty_tag_t{}; +namespace async { + /** * @brief Represents the step an std::async is at. */ -enum class async_state_t { +enum class state_t { sent, /* Request was sent but not co_await-ed. handle is nullptr, result_storage is not constructed */ waiting, /* Request was co_await-ed. handle is valid, result_storage is not constructed */ done, /* Request was completed. handle is unknown, result_storage is valid */ @@ -65,7 +67,7 @@ struct async_callback_data { /** * @brief State of the awaitable and the API callback */ - std::atomic state = detail::async_state_t::sent; + std::atomic state = state_t::sent; /** * @brief The stored result of the API call, stored as an array of bytes to directly construct in place @@ -96,17 +98,17 @@ struct async_callback_data { * Also destroys the result if present. */ ~async_callback_data() { - if (state.load() == detail::async_state_t::done) { + if (state.load() == state_t::done) { std::destroy_at(reinterpret_cast(result_storage.data())); } } }; /** - * @brief Base class of dpp::async. + * @brief Base class of dpp::async. * - * @warning This class should not be used directly by a user, use dpp::async instead. - * @note This class contains all the functions used internally by co_await. It is intentionally opaque and a private base of dpp::async so a user cannot call await_suspend and await_resume directly. + * @warning This class should not be used directly by a user, use dpp::async instead. + * @note This class contains all the functions used internally by co_await. It is intentionally opaque and a private base of dpp::async so a user cannot call await_suspend and await_resume directly. */ template class async_base { @@ -117,7 +119,7 @@ class async_base { /** * @brief Self-managed ref-counted pointer to the state data */ - detail::async_callback_data *state = new detail::async_callback_data; + async_callback_data *state = new async_callback_data; /** * @brief Callback function. @@ -129,7 +131,7 @@ class async_base { template V> void operator()(V &&cback) const { state->construct_result(std::forward(cback)); - if (auto previous_state = state->state.exchange(detail::async_state_t::done); previous_state == detail::async_state_t::waiting) { + if (auto previous_state = state->state.exchange(state_t::done); previous_state == state_t::waiting) { state->coro_handle.resume(); } } @@ -162,8 +164,9 @@ class async_base { * @brief Destructor. Releases the held reference and destroys if no other references exist. */ ~shared_callback() { - if (!state) // Moved-from object + if (!state) { // Moved-from object return; + } auto count = state->ref_count.fetch_sub(1); if (count == 0) { @@ -192,19 +195,20 @@ class async_base { * @brief Function called by the async when it is destroyed when it was never co_awaited, signals to the callback to abort. */ void set_dangling() noexcept { - if (!state) // moved-from object + if (!state) { // moved-from object return; - state->state.store(detail::async_state_t::dangling); + } + state->state.store(state_t::dangling); } bool done(std::memory_order order = std::memory_order_seq_cst) const noexcept { - return (state->state.load(order) == detail::async_state_t::done); + return (state->state.load(order) == state_t::done); } /** * @brief Convenience function to get the shared callback state's result. * - * @warning It is UB to call this on a callback whose state is anything else but async_state_t::done. + * @warning It is UB to call this on a callback whose state is anything else but state_t::done. */ R &get_result() noexcept { assert(state && done()); @@ -214,7 +218,7 @@ class async_base { /** * @brief Convenience function to get the shared callback state's result. * - * @warning It is UB to call this on a callback whose state is anything else but async_state_t::done. + * @warning It is UB to call this on a callback whose state is anything else but state_t::done. */ const R &get_result() const noexcept { assert(state && done()); @@ -304,7 +308,7 @@ class async_base { * * @return bool Whether we already have the result of the API call or not */ - bool await_ready() const noexcept { + [[nodiscard]] bool await_ready() const noexcept { return api_callback.done(); } @@ -316,17 +320,17 @@ class async_base { * @remark Do not call this manually, use the co_await keyword instead. * @param caller The handle to the coroutine co_await-ing and being suspended */ - bool await_suspend(detail::std_coroutine::coroutine_handle<> caller) noexcept { - auto sent = detail::async_state_t::sent; + [[nodiscard]] bool await_suspend(detail::std_coroutine::coroutine_handle<> caller) noexcept { + auto sent = state_t::sent; api_callback.state->coro_handle = caller; - return api_callback.state->state.compare_exchange_strong(sent, detail::async_state_t::waiting); // true (suspend) if `sent` was replaced with `waiting` -- false (resume) if the value was not `sent` (`done` is the only other option) + return api_callback.state->state.compare_exchange_strong(sent, state_t::waiting); // true (suspend) if `sent` was replaced with `waiting` -- false (resume) if the value was not `sent` (`done` is the only other option) } /** * @brief Function called by the standard library when the async is resumed. Its return value is what the whole co_await expression evaluates to * * @remark Do not call this manually, use the co_await keyword instead. - * @return R& The result of the API call as an lvalue reference. + * @return The result of the API call as an lvalue reference. */ R& await_resume() & noexcept { return api_callback.get_result(); @@ -337,7 +341,7 @@ class async_base { * @brief Function called by the standard library when the async is resumed. Its return value is what the whole co_await expression evaluates to * * @remark Do not call this manually, use the co_await keyword instead. - * @return const R& The result of the API call as a const lvalue reference. + * @return The result of the API call as a const lvalue reference. */ const R& await_resume() const& noexcept { return api_callback.get_result(); @@ -347,18 +351,20 @@ class async_base { * @brief Function called by the standard library when the async is resumed. Its return value is what the whole co_await expression evaluates to * * @remark Do not call this manually, use the co_await keyword instead. - * @return R&& The result of the API call as an rvalue reference. + * @return The result of the API call as an rvalue reference. */ R&& await_resume() && noexcept { return std::move(api_callback.get_result()); } }; +} // namespace async + } // namespace detail struct confirmation_callback_t; -/** +/** @class async async.h coro/async.h * @brief A co_await-able object handling an API call in parallel with the caller. * * This class is the return type of the dpp::cluster::co_* methods, but it can also be created manually to wrap any async call. @@ -368,19 +374,19 @@ struct confirmation_callback_t; * @tparam R The return type of the API call. Defaults to confirmation_callback_t */ template -class async : private detail::async_base { +class async : private detail::async::async_base { /** * @brief Internal use only base class. It serves to prevent await_suspend and await_resume from being used directly. * * @warning For internal use only, do not use. * @see operator co_await() */ - friend class detail::async_base; + friend class detail::async::async_base; public: - using detail::async_base::async_base; // use async_base's constructors. unfortunately on clang this doesn't include the templated ones so we have to delegate below - using detail::async_base::operator=; // use async_base's assignment operator - using detail::async_base::await_ready; // expose await_ready as public + using detail::async::async_base::async_base; // use async_base's constructors. unfortunately on clang this doesn't include the templated ones so we have to delegate below + using detail::async::async_base::operator=; // use async_base's assignment operator + using detail::async::async_base::await_ready; // expose await_ready as public /** * @brief Construct an async object wrapping an object method, the call is made immediately by forwarding to std::invoke and can be awaited later to retrieve the result. @@ -393,7 +399,7 @@ class async : private detail::async_base { #ifndef _DOXYGEN_ requires std::invocable> #endif - explicit async(Obj &&obj, Fun &&fun, Args&&... args) : detail::async_base{std::forward(obj), std::forward(fun), std::forward(args)...} {} + explicit async(Obj &&obj, Fun &&fun, Args&&... args) : detail::async::async_base{std::forward(obj), std::forward(fun), std::forward(args)...} {} /** * @brief Construct an async object wrapping an invokeable object, the call is made immediately by forwarding to std::invoke and can be awaited later to retrieve the result. @@ -405,7 +411,7 @@ class async : private detail::async_base { #ifndef _DOXYGEN_ requires std::invocable> #endif - explicit async(Fun &&fun, Args&&... args) : detail::async_base{std::forward(fun), std::forward(args)...} {} + explicit async(Fun &&fun, Args&&... args) : detail::async::async_base{std::forward(fun), std::forward(args)...} {} #ifdef _DOXYGEN_ // :) /** @@ -453,34 +459,34 @@ class async : private detail::async_base { * * @return bool Whether we already have the result of the API call or not */ - bool await_ready() const noexcept; + [[nodiscard]] bool await_ready() const noexcept; #endif /** * @brief Suspend the caller until the request completes. * - * @return R& On resumption, this expression evaluates to the result object of type R, as a reference. + * @return On resumption, this expression evaluates to the result object of type R, as a reference. */ - auto& operator co_await() & noexcept { - return static_cast&>(*this); + [[nodiscard]] auto& operator co_await() & noexcept { + return static_cast&>(*this); } /** * @brief Suspend the caller until the request completes. * - * @return const R& On resumption, this expression evaluates to the result object of type R, as a const reference. + * @return On resumption, this expression evaluates to the result object of type R, as a const reference. */ - const auto& operator co_await() const & noexcept { - return static_cast const&>(*this); + [[nodiscard]] const auto& operator co_await() const & noexcept { + return static_cast const&>(*this); } /** * @brief Suspend the caller until the request completes. * - * @return R&& On resumption, this expression evaluates to the result object of type R, as an rvalue reference. + * @return On resumption, this expression evaluates to the result object of type R, as an rvalue reference. */ - auto&& operator co_await() && noexcept { - return static_cast&&>(*this); + [[nodiscard]] auto&& operator co_await() && noexcept { + return static_cast&&>(*this); } }; diff --git a/include/dpp/coro/coroutine.h b/include/dpp/coro/coroutine.h index 303c484762..38b80b9127 100644 --- a/include/dpp/coro/coroutine.h +++ b/include/dpp/coro/coroutine.h @@ -34,14 +34,16 @@ namespace dpp { namespace detail { +namespace coroutine { + template -struct coroutine_promise; +struct promise_t; template /** - * @brief Alias for the coroutine_handle of a coroutine. + * @brief Alias for the handle_t of a coroutine. */ -using coroutine_handle = std_coroutine::coroutine_handle>; +using handle_t = std_coroutine::coroutine_handle>; /** * @brief Base class of dpp::coroutine. @@ -55,18 +57,18 @@ class coroutine_base { /** * @brief Promise has friend access for the constructor */ - friend struct detail::coroutine_promise; + friend struct promise_t; /** * @brief Coroutine handle. */ - detail::coroutine_handle handle{nullptr}; + detail::coroutine::handle_t handle{nullptr}; private: /** * @brief Construct from a handle. Internal use only. */ - coroutine_base(detail::coroutine_handle h) : handle{h} {} + coroutine_base(detail::coroutine::handle_t h) : handle{h} {} public: /** @@ -90,8 +92,9 @@ class coroutine_base { * @brief Destructor, destroys the handle. */ ~coroutine_base() { - if (handle) + if (handle) { handle.destroy(); + } } /** @@ -116,9 +119,10 @@ class coroutine_base { * @throws invalid_operation_exception if the coroutine is empty or finished. * @return bool Whether the coroutine is done */ - bool await_ready() const { - if (!handle) + [[nodiscard]] bool await_ready() const { + if (!handle) { throw dpp::logic_exception("cannot co_await an empty coroutine"); + } return handle.done(); } @@ -131,7 +135,7 @@ class coroutine_base { * @param caller The calling coroutine, now suspended */ template - detail::coroutine_handle await_suspend(detail::std_coroutine::coroutine_handle caller) noexcept { + [[nodiscard]] handle_t await_suspend(detail::std_coroutine::coroutine_handle caller) noexcept { handle.promise().parent = caller; return handle; } @@ -144,7 +148,7 @@ class coroutine_base { * @return R The result of the coroutine. It is given to the caller as a result to `co_await` */ decltype(auto) await_resume() & { - return static_cast &>(*this).await_resume_impl(); + return static_cast &>(*this).await_resume_impl(); } /** @@ -154,8 +158,8 @@ class coroutine_base { * @throw Throws any exception thrown or uncaught by the coroutine * @return R The result of the coroutine. It is given to the caller as a result to `co_await` */ - decltype(auto) await_resume() const & { - return static_cast const&>(*this).await_resume_impl(); + [[nodiscard]] decltype(auto) await_resume() const & { + return static_cast const&>(*this).await_resume_impl(); } /** @@ -165,48 +169,54 @@ class coroutine_base { * @throw Throws any exception thrown or uncaught by the coroutine * @return R The result of the coroutine. It is given to the caller as a result to `co_await` */ - decltype(auto) await_resume() && { - return static_cast &&>(*this).await_resume_impl(); + [[nodiscard]] decltype(auto) await_resume() && { + return static_cast &&>(*this).await_resume_impl(); } }; +} // namespace coroutine + } // namespace detail -/** +/** @class coroutine coroutine.h coro/coroutine.h * @brief Base type for a coroutine, starts on co_await. * - * @warning - This feature is EXPERIMENTAL. The API may change at any time and there may be bugs. Please report any to GitHub issues or to the D++ Discord server. + * @warning - This feature is EXPERIMENTAL. The API may change at any time and there may be bugs. + * Please report any to GitHub Issues or to our Discord Server. * @warning - Using co_await on this object more than once is undefined behavior. * @tparam R Return type of the coroutine. Can be void, or a complete object that supports move construction and move assignment. */ template -class coroutine : private detail::coroutine_base { +class coroutine : private detail::coroutine::coroutine_base { /** * @brief Internal use only base class containing common logic between coroutine and coroutine. It also serves to prevent await_suspend and await_resume from being used directly. * * @warning For internal use only, do not use. * @see operator co_await() */ - friend class detail::coroutine_base; + friend class detail::coroutine::coroutine_base; - R& await_resume_impl() & { - detail::coroutine_promise &promise = this->handle.promise(); - if (promise.exception) + [[nodiscard]] R& await_resume_impl() & { + detail::coroutine::promise_t &promise = this->handle.promise(); + if (promise.exception) { std::rethrow_exception(promise.exception); + } return *promise.result; } - const R& await_resume_impl() const & { - detail::coroutine_promise &promise = this->handle.promise(); - if (promise.exception) + [[nodiscard]] const R& await_resume_impl() const & { + detail::coroutine::promise_t &promise = this->handle.promise(); + if (promise.exception) { std::rethrow_exception(promise.exception); + } return *promise.result; } - R&& await_resume_impl() && { - detail::coroutine_promise &promise = this->handle.promise(); - if (promise.exception) + [[nodiscard]] R&& await_resume_impl() && { + detail::coroutine::promise_t &promise = this->handle.promise(); + if (promise.exception) { std::rethrow_exception(promise.exception); + } return *std::move(promise.result); } @@ -253,41 +263,41 @@ class coroutine : private detail::coroutine_base { * @throws invalid_operation_exception if the coroutine is empty or finished. * @return bool Whether the coroutine is done */ - bool await_ready() const; + [[nodiscard]] bool await_ready() const; #else - using detail::coroutine_base::coroutine_base; // use coroutine_base's constructors - using detail::coroutine_base::operator=; // use coroutine_base's assignment operators - using detail::coroutine_base::await_ready; // expose await_ready as public + using detail::coroutine::coroutine_base::coroutine_base; // use coroutine_base's constructors + using detail::coroutine::coroutine_base::operator=; // use coroutine_base's assignment operators + using detail::coroutine::coroutine_base::await_ready; // expose await_ready as public #endif /** * @brief Suspend the caller until the coroutine completes. * * @throw On resumption, any exception thrown by the coroutine is propagated to the caller. - * @return R& On resumption, this expression evaluates to the result object of type R, as a reference. + * @return On resumption, this expression evaluates to the result object of type R, as a reference. */ - auto& operator co_await() & noexcept { - return static_cast&>(*this); + [[nodiscard]] auto& operator co_await() & noexcept { + return static_cast&>(*this); } /** * @brief Suspend the caller until the coroutine completes. * * @throw On resumption, any exception thrown by the coroutine is propagated to the caller. - * @return const R& On resumption, this expression evaluates to the result object of type R, as a const reference. + * @return On resumption, this expression evaluates to the result object of type R, as a const reference. */ - const auto& operator co_await() const & noexcept { - return static_cast const&>(*this); + [[nodiscard]] const auto& operator co_await() const & noexcept { + return static_cast const&>(*this); } /** * @brief Suspend the caller until the coroutine completes. * * @throw On resumption, any exception thrown by the coroutine is propagated to the caller. - * @return R&& On resumption, this expression evaluates to the result object of type R, as an rvalue reference. + * @return On resumption, this expression evaluates to the result object of type R, as an rvalue reference. */ - auto&& operator co_await() && noexcept { - return static_cast&&>(*this); + [[nodiscard]] auto&& operator co_await() && noexcept { + return static_cast&&>(*this); } }; @@ -300,64 +310,64 @@ class coroutine : private detail::coroutine_base { * @tparam R Return type of the coroutine. Can be void, or a complete object that supports move construction and move assignment. */ template <> -class coroutine : private detail::coroutine_base { +class coroutine : private detail::coroutine::coroutine_base { /** * @brief Base class has friend access for CRTP downcast */ - friend class detail::coroutine_base; + friend class detail::coroutine::coroutine_base; void await_resume_impl() const; public: - using detail::coroutine_base::coroutine_base; // use coroutine_base's constructors - using detail::coroutine_base::operator=; // use coroutine_base's assignment operators - using detail::coroutine_base::await_ready; // expose await_ready as public + using detail::coroutine::coroutine_base::coroutine_base; // use coroutine_base's constructors + using detail::coroutine::coroutine_base::operator=; // use coroutine_base's assignment operators + using detail::coroutine::coroutine_base::await_ready; // expose await_ready as public /** * @brief Suspend the current coroutine until the coroutine completes. * * @throw On resumption, any exception thrown by the coroutine is propagated to the caller. - * @return R& On resumption, this expression evaluates to the result object of type R, as a reference. + * @return On resumption, this expression evaluates to the result object of type R, as a reference. */ - auto& operator co_await() & noexcept { - return static_cast&>(*this); + [[nodiscard]] auto& operator co_await() & noexcept { + return static_cast&>(*this); } /** * @brief Suspend the current coroutine until the coroutine completes. * * @throw On resumption, any exception thrown by the coroutine is propagated to the caller. - * @return const R& On resumption, this expression evaluates to the result object of type R, as a const reference. + * @return On resumption, this expression evaluates to the result object of type R, as a const reference. */ - const auto& operator co_await() const & noexcept { - return static_cast const &>(*this); + [[nodiscard]] const auto& operator co_await() const & noexcept { + return static_cast const &>(*this); } /** * @brief Suspend the current coroutine until the coroutine completes. * * @throw On resumption, any exception thrown by the coroutine is propagated to the caller. - * @return R&& On resumption, this expression evaluates to the result object of type R, as an rvalue reference. + * @return On resumption, this expression evaluates to the result object of type R, as an rvalue reference. */ - auto&& operator co_await() && noexcept { - return static_cast&&>(*this); + [[nodiscard]] auto&& operator co_await() && noexcept { + return static_cast&&>(*this); } }; #endif /* _DOXYGEN_ */ -namespace detail { +namespace detail::coroutine { template - struct coroutine_final_awaiter; + struct final_awaiter; #ifdef DPP_CORO_TEST - struct coroutine_promise_base{}; + struct promise_t_base{}; #endif /** * @brief Promise type for coroutine. */ template - struct coroutine_promise { + struct promise_t { /** * @brief Handle of the coroutine co_await-ing this coroutine. */ @@ -374,28 +384,28 @@ namespace detail { std::exception_ptr exception{nullptr}; #ifdef DPP_CORO_TEST - coroutine_promise() { - ++coro_alloc_count; + promise_t() { + ++coro_alloc_count; } - ~coroutine_promise() { - --coro_alloc_count; + ~promise_t() { + --coro_alloc_count; } #endif /** * @brief Function called by the standard library when reaching the end of a coroutine * - * @return coroutine_final_awaiter Resumes any coroutine co_await-ing on this + * @return final_awaiter Resumes any coroutine co_await-ing on this */ - coroutine_final_awaiter final_suspend() const noexcept; + [[nodiscard]] final_awaiter final_suspend() const noexcept; /** * @brief Function called by the standard library when the coroutine start * - * @return suspend_always Always suspend at the start, for a lazy start + * @return @return std::suspend_always Always suspend at the start, for a lazy start */ - std_coroutine::suspend_always initial_suspend() const noexcept { + [[nodiscard]] std_coroutine::suspend_always initial_suspend() const noexcept { return {}; } @@ -446,8 +456,8 @@ namespace detail { /** * @brief Function called to get the coroutine object */ - coroutine get_return_object() { - return coroutine{coroutine_handle::from_promise(*this)}; + dpp::coroutine get_return_object() { + return dpp::coroutine{handle_t::from_promise(*this)}; } }; @@ -455,22 +465,22 @@ namespace detail { * @brief Struct returned by a coroutine's final_suspend, resumes the continuation */ template - struct coroutine_final_awaiter { + struct final_awaiter { /** * @brief First function called by the standard library when reaching the end of a coroutine * * @return false Always return false, we need to suspend to resume the parent */ - bool await_ready() const noexcept { + [[nodiscard]] bool await_ready() const noexcept { return false; } /** * @brief Second function called by the standard library when reaching the end of a coroutine. * - * @return std::coroutine_handle<> Coroutine handle to resume, this is either the parent if present or std::noop_coroutine() + * @return std::handle_t<> Coroutine handle to resume, this is either the parent if present or std::noop_coroutine() */ - std_coroutine::coroutine_handle<> await_suspend(std_coroutine::coroutine_handle> handle) const noexcept { + [[nodiscard]] std_coroutine::coroutine_handle<> await_suspend(std_coroutine::coroutine_handle> handle) const noexcept { auto parent = handle.promise().parent; return parent ? parent : std_coroutine::noop_coroutine(); @@ -483,7 +493,7 @@ namespace detail { }; template - coroutine_final_awaiter coroutine_promise::final_suspend() const noexcept { + final_awaiter promise_t::final_suspend() const noexcept { return {}; } @@ -491,7 +501,7 @@ namespace detail { * @brief Struct returned by a coroutine's final_suspend, resumes the continuation */ template <> - struct coroutine_promise { + struct promise_t { /** * @brief Handle of the coroutine co_await-ing this coroutine. */ @@ -505,18 +515,18 @@ namespace detail { /** * @brief Function called by the standard library when reaching the end of a coroutine * - * @return coroutine_final_awaiter Resumes any coroutine co_await-ing on this + * @return final_awaiter Resumes any coroutine co_await-ing on this */ - coroutine_final_awaiter final_suspend() const noexcept { + [[nodiscard]] final_awaiter final_suspend() const noexcept { return {}; } /** * @brief Function called by the standard library when the coroutine start * - * @return suspend_always Always suspend at the start, for a lazy start + * @return @return std::suspend_always Always suspend at the start, for a lazy start */ - std_coroutine::suspend_always initial_suspend() const noexcept { + [[nodiscard]] std_coroutine::suspend_always initial_suspend() const noexcept { return {}; } @@ -537,8 +547,8 @@ namespace detail { /** * @brief Function called to get the coroutine object */ - coroutine get_return_object() { - return coroutine{coroutine_handle::from_promise(*this)}; + [[nodiscard]] dpp::coroutine get_return_object() { + return dpp::coroutine{handle_t::from_promise(*this)}; } }; @@ -546,8 +556,9 @@ namespace detail { #ifndef _DOXYGEN_ inline void coroutine::await_resume_impl() const { - if (handle.promise().exception) + if (handle.promise().exception) { std::rethrow_exception(handle.promise().exception); + } } #endif /* _DOXYGEN_ */ @@ -558,7 +569,7 @@ inline void coroutine::await_resume_impl() const { */ template struct dpp::detail::std_coroutine::coroutine_traits, Args...> { - using promise_type = dpp::detail::coroutine_promise; + using promise_type = dpp::detail::coroutine::promise_t; }; #endif /* DPP_CORO */ diff --git a/include/dpp/coro/job.h b/include/dpp/coro/job.h index 86d763b01b..e2473f39c7 100644 --- a/include/dpp/coro/job.h +++ b/include/dpp/coro/job.h @@ -29,15 +29,16 @@ namespace dpp { -/** - * @brief Extremely light coroutine object designed to send off a coroutine to execute on its own. It can be attached to an event router using dpp::event_router_t::co_attach. +/** @class job job.h coro/job.h + * @brief Extremely light coroutine object designed to send off a coroutine to execute on its own. + * Can be used in conjunction with coroutine events via @ref dpp::event_router_t::operator()(F&&) "event routers", or on its own. * * This object stores no state and is the recommended way to use coroutines if you do not need to co_await the result. * - * @warning - This feature is EXPERIMENTAL. The API may change at any time and there may be bugs. Please report any to GitHub issues or to the D++ Discord server. + * @warning - This feature is EXPERIMENTAL. The API may change at any time and there may be bugs. + * Please report any to GitHub Issues or to our Discord Server. * @warning - 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. + * At this point, if the function returns, every object declared in the function including its parameters are destroyed, which causes @ref lambdas-and-locals "dangling references". * For this reason, `co_await` will error if any parameters are passed by reference. * 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. @@ -46,6 +47,8 @@ struct job {}; namespace detail { +namespace job { + template inline constexpr bool coroutine_has_no_ref_params_v = false; @@ -56,21 +59,21 @@ 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 job_promise_base{}; + struct promise{}; #endif /** * @brief Coroutine promise type for a job */ template -struct job_promise { +struct promise { #ifdef DPP_CORO_TEST - job_promise() { + promise() { ++coro_alloc_count; } - ~job_promise() { + ~promise() { --coro_alloc_count; } #endif @@ -78,7 +81,7 @@ struct job_promise { /* * @brief Function called when the job is done. * - * @return Do not suspend at the end, destroying the handle immediately + * @return std::suspend_never Do not suspend at the end, destroying the handle immediately */ std_coroutine::suspend_never final_suspend() const noexcept { return {}; @@ -87,7 +90,7 @@ struct job_promise { /* * @brief Function called when the job is started. * - * @return Do not suspend at the start, starting the job immediately + * @return std::suspend_never Do not suspend at the start, starting the job immediately */ std_coroutine::suspend_never initial_suspend() const noexcept { return {}; @@ -116,6 +119,9 @@ struct job_promise { */ 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 { /** @@ -133,6 +139,8 @@ struct job_promise { } }; +} // namespace job + } // namespace detail } // namespace dpp @@ -148,7 +156,7 @@ struct dpp::detail::std_coroutine::coroutine_traits { * When the coroutine is created from a lambda, that lambda is passed as a first parameter. * Not ideal but we'll allow any callable that takes the rest of the arguments passed */ - using promise_type = dpp::detail::job_promise; + using promise_type = dpp::detail::job::promise; }; #endif /* DPP_CORO */ diff --git a/include/dpp/coro/task.h b/include/dpp/coro/task.h index 811aa4677f..5960e0b724 100644 --- a/include/dpp/coro/task.h +++ b/include/dpp/coro/task.h @@ -38,7 +38,11 @@ namespace dpp { namespace detail { -enum class task_state_t { +/* Internal cogwheels for dpp::task */ +namespace task { + +/** @brief State of a task */ +enum class state_t { /** @brief Task was started but never co_await-ed */ started, /** @brief Task was co_await-ed and is pending completion */ @@ -50,28 +54,34 @@ enum class task_state_t { }; /** - * @brief A task's promise type, with special logic for handling nested tasks. + * @brief A @ref dpp::task "task"'s promise_t type, with special logic for handling nested tasks. + * + * @tparam R Return type of the task */ template -struct task_promise; +struct promise_t; /** - * @brief The object automatically co_await-ed at the end of a task. Ensures nested task chains are resolved, and the promise cleans up if it needs to. + * @brief The object automatically co_await-ed at the end of a @ref dpp::task "task". Ensures nested coroutine chains are resolved, and the promise_t cleans up if it needs to. + * + * @tparam R Return type of the task */ template -struct task_chain_final_awaiter; +struct final_awaiter; /** - * @brief Alias for std::coroutine_handle for a task_promise. + * @brief Alias for for a @ref dpp::task "task"'s @ref promise_t. + * + * @tparam R Return type of the task */ template -using task_handle = detail::std_coroutine::coroutine_handle>; +using handle_t = std_coroutine::coroutine_handle>; /** - * @brief Base class of dpp::task. + * @brief Base class of @ref dpp::task. * - * @warning This class should not be used directly by a user, use dpp::task instead. - * @note This class contains all the functions used internally by co_await. It is intentionally opaque and a private base of dpp::task so a user cannot call await_suspend and await_resume directly. + * @warning This class should not be used directly by a user, use @ref dpp::task instead. + * @note This class contains all the functions used internally by co_await. It is intentionally opaque and a private base of @ref dpp::task so a user cannot call await_suspend() and await_resume() directly. */ template class task_base { @@ -79,18 +89,18 @@ class task_base { /** * @brief The coroutine handle of this task. */ - detail::task_handle handle; + handle_t handle; /** * @brief Promise type of this coroutine. For internal use only, do not use. */ - friend struct detail::task_promise; + friend struct promise_t; private: /** * @brief Construct from a coroutine handle. Internal use only */ - explicit task_base(detail::task_handle handle_) : handle(handle_) {} + explicit task_base(handle_t handle_) : handle(handle_) {} public: /** @@ -118,13 +128,15 @@ class task_base { */ ~task_base() { if (handle) { - detail::task_promise &promise = handle.promise(); - detail::task_state_t previous_state = promise.state.exchange(detail::task_state_t::dangling); + promise_t &promise = handle.promise(); + state_t previous_state = promise.state.exchange(state_t::dangling); - if (previous_state == detail::task_state_t::done) + if (previous_state == state_t::done) { handle.destroy(); - else + } + else { cancel(); + } } } @@ -150,10 +162,11 @@ class task_base { * @throws logic_exception if the task is empty. * @return bool Whether not to suspend the caller or not */ - bool await_ready() const { - if (!handle) + [[nodiscard]] bool await_ready() const { + if (!handle) { throw dpp::logic_exception{"cannot co_await an empty task"}; - return handle.promise().state.load() == detail::task_state_t::done; + } + return handle.promise().state.load() == state_t::done; } /** @@ -165,14 +178,15 @@ class task_base { * @param caller The calling coroutine, now suspended * @return bool Whether to suspend the caller or not */ - bool await_suspend(detail::std_coroutine::coroutine_handle<> caller) { - detail::task_promise &my_promise = handle.promise(); - auto previous_state = detail::task_state_t::started; + [[nodiscard]] bool await_suspend(std_coroutine::coroutine_handle<> caller) noexcept { + promise_t &my_promise = handle.promise(); + auto previous_state = state_t::started; my_promise.parent = caller; // Replace `sent` state with `awaited` ; if that fails, the only logical option is the state was `done`, in which case return false to resume - if (!handle.promise().state.compare_exchange_strong(previous_state, detail::task_state_t::awaited) && previous_state == detail::task_state_t::done) + if (!handle.promise().state.compare_exchange_strong(previous_state, state_t::awaited) && previous_state == state_t::done) { return false; + } return true; } @@ -182,86 +196,92 @@ class task_base { * @return bool Whether the task is finished. */ [[nodiscard]] bool done() const noexcept { - return handle && handle.promise().state.load(std::memory_order_relaxed) == detail::task_state_t::done; + return handle && handle.promise().state.load(std::memory_order_relaxed) == state_t::done; } /** * @brief Cancel the task, it will stop the next time it uses co_await. On co_await-ing this task, throws dpp::task_cancelled_exception. + * + * @return *this */ dpp::task& cancel() & noexcept { handle.promise().cancelled.exchange(true, std::memory_order_relaxed); - return static_cast &>(*this); + return static_cast &>(*this); } /** * @brief Cancel the task, it will stop the next time it uses co_await. On co_await-ing this task, throws dpp::task_cancelled_exception. + * + * @return *this */ dpp::task&& cancel() && noexcept { handle.promise().cancelled.exchange(true, std::memory_order_relaxed); - return static_cast &&>(*this); + return static_cast &&>(*this); } /** * @brief Function called by the standard library when resuming. * - * @return R& Return value of the coroutine, handed to the caller of co_await. + * @return Return value of the coroutine, handed to the caller of co_await. */ decltype(auto) await_resume() & { - return static_cast &>(*this).await_resume_impl(); + return static_cast &>(*this).await_resume_impl(); } /** * @brief Function called by the standard library when resuming. * - * @return const R& Return value of the coroutine, handed to the caller of co_await. + * @return Return value of the coroutine, handed to the caller of co_await. */ decltype(auto) await_resume() const & { - return static_cast &>(*this).await_resume_impl(); + return static_cast &>(*this).await_resume_impl(); } /** * @brief Function called by the standard library when resuming. * - * @return R&& Return value of the coroutine, handed to the caller of co_await. + * @return Return value of the coroutine, handed to the caller of co_await. */ decltype(auto) await_resume() && { - return static_cast &&>(*this).await_resume_impl(); + return static_cast &&>(*this).await_resume_impl(); } }; +} // namespace task + } // namespace detail -/** +/** @class task task.h coro/task.h * @brief A coroutine task. It starts immediately on construction and can be co_await-ed, making it perfect for parallel coroutines returning a value. * - * Can be used in conjunction with coroutine events via dpp::event_router_t::co_attach, or on its own. - * - * @warning - This feature is EXPERIMENTAL. The API may change at any time and there may be bugs. Please report any to GitHub issues or to the D++ Discord server. - * @tparam R Return type of the coroutine. Cannot be a reference, can be void. + * @warning - This feature is EXPERIMENTAL. The API may change at any time and there may be bugs. + * Please report any to GitHub Issues or to our Discord Server. + * @tparam R Return type of the task. Cannot be a reference but can be void. */ template #ifndef _DOXYGEN_ requires (!std::is_reference_v) #endif -class task : private detail::task_base { +class task : private detail::task::task_base { /** * @brief Internal use only base class containing common logic between task and task. It also serves to prevent await_suspend and await_resume from being used directly. * * @warning For internal use only, do not use. * @see operator co_await() */ - friend class detail::task_base; + friend class detail::task::task_base; /** * @brief Function called by the standard library when the coroutine is resumed. * * @throw Throws any exception thrown or uncaught by the coroutine - * @return R& The result of the coroutine. This is returned to the awaiter as the result of co_await + * @return The result of the coroutine. This is returned to the awaiter as the result of co_await */ R& await_resume_impl() & { - detail::task_promise &promise = this->handle.promise(); - if (promise.exception) + detail::task::promise_t &promise = this->handle.promise(); + if (promise.exception) { std::rethrow_exception(promise.exception); + } return *reinterpret_cast(promise.result_storage.data()); } @@ -269,12 +289,13 @@ class task : private detail::task_base { * @brief Function called by the standard library when the coroutine is resumed. * * @throw Throws any exception thrown or uncaught by the coroutine - * @return const R& The result of the coroutine. This is returned to the awaiter as the result of co_await + * @return The result of the coroutine. This is returned to the awaiter as the result of co_await */ const R& await_resume_impl() const & { - detail::task_promise &promise = this->handle.promise(); - if (promise.exception) + detail::task::promise_t &promise = this->handle.promise(); + if (promise.exception) { std::rethrow_exception(promise.exception); + } return *reinterpret_cast(promise.result_storage.data()); } @@ -282,12 +303,13 @@ class task : private detail::task_base { * @brief Function called by the standard library when the coroutine is resumed. * * @throw Throws any exception thrown or uncaught by the coroutine - * @return R&& The result of the coroutine. This is returned to the awaiter as the result of co_await + * @return The result of the coroutine. This is returned to the awaiter as the result of co_await */ R&& await_resume_impl() && { - detail::task_promise &promise = this->handle.promise(); - if (promise.exception) + detail::task::promise_t &promise = this->handle.promise(); + if (promise.exception) { std::rethrow_exception(promise.exception); + } return *reinterpret_cast(promise.result_storage.data()); } @@ -349,43 +371,43 @@ class task : private detail::task_base { * @throws logic_exception if the task is empty. * @return bool Whether not to suspend the caller or not */ - bool await_ready() const; + [[nodiscard]] bool await_ready() const; #else - using detail::task_base::task_base; // use task_base's constructors - using detail::task_base::operator=; // use task_base's assignment operators - using detail::task_base::done; // expose done() as public - using detail::task_base::cancel; // expose cancel() as public - using detail::task_base::await_ready; // expose await_ready as public + using detail::task::task_base::task_base; // use task_base's constructors + using detail::task::task_base::operator=; // use task_base's assignment operators + using detail::task::task_base::done; // expose done() as public + using detail::task::task_base::cancel; // expose cancel() as public + using detail::task::task_base::await_ready; // expose await_ready as public #endif /** * @brief Suspend the current coroutine until the task completes. * * @throw On resumption, any exception thrown by the coroutine is propagated to the caller. - * @return R& On resumption, this expression evaluates to the result object of type R, as a reference. + * @return On resumption, this expression evaluates to the result object of type R, as a reference. */ - auto& operator co_await() & noexcept { - return static_cast&>(*this); + [[nodiscard]] auto& operator co_await() & noexcept { + return static_cast&>(*this); } /** * @brief Suspend the current coroutine until the task completes. * * @throw On resumption, any exception thrown by the coroutine is propagated to the caller. - * @return const R& On resumption, this expression evaluates to the result object of type R, as a const reference. + * @return On resumption, this expression evaluates to the result object of type R, as a const reference. */ - const auto& operator co_await() const & noexcept { - return static_cast&>(*this); + [[nodiscard]] const auto& operator co_await() const & noexcept { + return static_cast&>(*this); } /** * @brief Suspend the current coroutine until the task completes. * * @throw On resumption, any exception thrown by the coroutine is propagated to the caller. - * @return R&& On resumption, this expression evaluates to the result object of type R, as an rvalue reference. + * @return On resumption, this expression evaluates to the result object of type R, as an rvalue reference. */ - auto&& operator co_await() && noexcept { - return static_cast&&>(*this); + [[nodiscard]] auto&& operator co_await() && noexcept { + return static_cast&&>(*this); } }; @@ -399,13 +421,13 @@ class task : private detail::task_base { * @tparam R Return type of the coroutine. Cannot be a reference, can be void. */ template <> -class task : private detail::task_base { +class task : private detail::task::task_base { /** * @brief Private base class containing common logic between task and task. It also serves to prevent await_suspend and await_resume from being used directly. * * @see operator co_await() */ - friend class detail::task_base; + friend class detail::task::task_base; /** * @brief Function called by the standard library when the coroutine is resumed. @@ -416,51 +438,54 @@ class task : private detail::task_base { void await_resume_impl() const; public: - using detail::task_base::task_base; // use task_base's constructors - using detail::task_base::operator=; // use task_base's assignment operators - using detail::task_base::done; // expose done() as public - using detail::task_base::cancel; // expose cancel() as public - using detail::task_base::await_ready; // expose await_ready as public + using detail::task::task_base::task_base; // use task_base's constructors + using detail::task::task_base::operator=; // use task_base's assignment operators + using detail::task::task_base::done; // expose done() as public + using detail::task::task_base::cancel; // expose cancel() as public + using detail::task::task_base::await_ready; // expose await_ready as public /** * @brief Suspend the current coroutine until the task completes. * * @throw On resumption, any exception thrown by the coroutine is propagated to the caller. + * @return On resumption, returns a reference to the contained result. */ auto& operator co_await() & { - return static_cast&>(*this); + return static_cast&>(*this); } /** * @brief Suspend the current coroutine until the task completes. * * @throw On resumption, any exception thrown by the coroutine is propagated to the caller. + * @return On resumption, returns a const reference to the contained result. */ const auto& operator co_await() const & { - return static_cast&>(*this); + return static_cast&>(*this); } /** * @brief Suspend the current coroutine until the task completes. * * @throw On resumption, any exception thrown by the coroutine is propagated to the caller. + * @return On resumption, returns a reference to the contained result. */ auto&& operator co_await() && { - return static_cast&&>(*this); + return static_cast&&>(*this); } }; #endif /* _DOXYGEN_ */ -namespace detail { +namespace detail::task { /** - * @brief Awaitable returned from task_promise's final_suspend. Resumes the parent and cleans up its handle if needed + * @brief Awaitable returned from task::promise_t's final_suspend. Resumes the parent and cleans up its handle if needed */ template -struct task_chain_final_awaiter { +struct final_awaiter { /** * @brief Always suspend at the end of the task. This allows us to clean up and resume the parent */ - bool await_ready() const noexcept { + [[nodiscard]] bool await_ready() const noexcept { return (false); } @@ -470,21 +495,22 @@ struct task_chain_final_awaiter { * @param handle The handle of this coroutine * @return std::coroutine_handle<> Handle to resume, which is either the parent if present or std::noop_coroutine() otherwise */ - std_coroutine::coroutine_handle<> await_suspend(detail::task_handle handle) const noexcept; + [[nodiscard]] std_coroutine::coroutine_handle<> await_suspend(handle_t handle) const noexcept; /* * @brief Function called when this object is co_awaited by the standard library at the end of final_suspend. Do nothing, return nothing */ void await_resume() const noexcept {} }; + /** - * @brief Base implementation of task_promise, without the logic that would depend on the return type. Meant to be inherited from + * @brief Base implementation of task::promise_t, without the logic that would depend on the return type. Meant to be inherited from */ -struct task_promise_base { +struct promise_base { /** * @brief State of the task, used to keep track of lifetime and status */ - std::atomic state = task_state_t::started; + std::atomic state = state_t::started; /** * @brief Whether the task is cancelled or not. @@ -499,17 +525,17 @@ struct task_promise_base { /** * @brief Exception ptr if any was thrown during the coroutine * - * @see std::exception_ptr + * @see std::suspend_never Don't suspend, the coroutine starts immediately. */ - std_coroutine::suspend_never initial_suspend() const noexcept { + [[nodiscard]] std_coroutine::suspend_never initial_suspend() const noexcept { return {}; } @@ -529,52 +555,79 @@ struct task_promise_base { */ void unhandled_exception() { exception = std::current_exception(); - if (state.load() == task_state_t::dangling && !cancelled) + if ((state.load() == task::state_t::dangling) && !cancelled) { throw; + } } + /** + * @brief Proxy awaitable that wraps any co_await inside the task and checks for cancellation on resumption + * + * @see await_transform + */ template struct proxy_awaiter { - const task_promise_base &promise; + /** @brief The promise_t object bound to this proxy */ + const task::promise_base &promise; + + /** @brief The inner awaitable being awaited */ A awaitable; - bool await_ready() noexcept(noexcept(awaitable.await_ready())) { + /** @brief Wrapper for the awaitable's await_ready */ + [[nodiscard]] bool await_ready() noexcept(noexcept(awaitable.await_ready())) { return awaitable.await_ready(); } + /** @brief Wrapper for the awaitable's await_suspend */ template - decltype(auto) await_suspend(T&& handle) noexcept(noexcept(awaitable.await_suspend(std::forward(handle)))) { + [[nodiscard]] decltype(auto) await_suspend(T&& handle) noexcept(noexcept(awaitable.await_suspend(std::forward(handle)))) { return awaitable.await_suspend(std::forward(handle)); } + /** + * @brief Wrapper for the awaitable's await_resume, throws if the task is cancelled + * + * @throw dpp::task_cancelled_exception If the task was cancelled + */ decltype(auto) await_resume() { - if (promise.cancelled.load()) + if (promise.cancelled.load()) { throw dpp::task_cancelled_exception{"task was cancelled"}; + } return awaitable.await_resume(); } }; + /** + * @brief Function called whenever co_await is used inside of the task + * + * @throw dpp::task_cancelled_exception On resumption if the task was cancelled + * + * @return @ref proxy_awaiter Returns a proxy awaiter that will check for cancellation on resumption + */ template - auto await_transform(T&& expr) const noexcept(noexcept(co_await_resolve(std::forward(expr)))) { + [[nodiscard]] auto await_transform(T&& expr) const noexcept(noexcept(co_await_resolve(std::forward(expr)))) { using awaitable_t = decltype(co_await_resolve(std::forward(expr))); return proxy_awaiter{*this, co_await_resolve(std::forward(expr))}; } }; /** - * @brief Implementation of task_promise for non-void return type + * @brief Implementation of task::promise_t for non-void return type */ template -struct task_promise : task_promise_base { - ~task_promise() { - if (state.load() == task_state_t::done && !exception) +struct promise_t : promise_base { + /** + * @brief Destructor. Destroys the value if it was constructed. + */ + ~promise_t() { + if (state.load() == state_t::done && !exception) { std::destroy_at(reinterpret_cast(result_storage.data())); + } } /** * @brief Stored return value of the coroutine. * - * @details The main reason we use std::optional here and not R is to avoid default construction of the value so we only require R to have a move constructor, instead of both a default constructor and move assignment operator */ alignas(R) std::array result_storage; @@ -616,27 +669,27 @@ struct task_promise : task_promise_base { /** * @brief Function called by the standard library when the coroutine is created. * - * @return task The coroutine object + * @return dpp::task The coroutine object */ - task get_return_object() noexcept { - return task{task_handle::from_promise(*this)}; + [[nodiscard]] dpp::task get_return_object() noexcept { + return dpp::task{handle_t::from_promise(*this)}; } /** * @brief Function called by the standard library when the coroutine reaches its last suspension point * - * @return task_chain_final_awaiter Special object containing the chain resolution and clean-up logic. + * @return final_awaiter Special object containing the chain resolution and clean-up logic. */ - task_chain_final_awaiter final_suspend() const noexcept { + [[nodiscard]] final_awaiter final_suspend() const noexcept { return {}; } }; /** - * @brief Implementation of task_promise for void return type + * @brief Implementation of task::promise_t for void return type */ template <> -struct task_promise : task_promise_base { +struct promise_t : promise_base { /** * @brief Function called by the standard library when the coroutine co_returns * @@ -649,34 +702,34 @@ struct task_promise : task_promise_base { * * @return task The coroutine object */ - task get_return_object() noexcept { - return task{task_handle::from_promise(*this)}; + [[nodiscard]] dpp::task get_return_object() noexcept { + return dpp::task{handle_t::from_promise(*this)}; } /** * @brief Function called by the standard library when the coroutine reaches its last suspension point * - * @return task_chain_final_awaiter Special object containing the chain resolution and clean-up logic. + * @return final_awaiter Special object containing the chain resolution and clean-up logic. */ - task_chain_final_awaiter final_suspend() const noexcept { + [[nodiscard]] final_awaiter final_suspend() const noexcept { return {}; } }; template -std_coroutine::coroutine_handle<> detail::task_chain_final_awaiter::await_suspend(task_handle handle) const noexcept { - task_promise &promise = handle.promise(); - task_state_t previous_state = promise.state.exchange(task_state_t::done); +std_coroutine::coroutine_handle<> final_awaiter::await_suspend(handle_t handle) const noexcept { + promise_t &promise = handle.promise(); + state_t previous_state = promise.state.exchange(state_t::done); switch (previous_state) { - case task_state_t::started: // started but never awaited, suspend + case state_t::started: // started but never awaited, suspend return std_coroutine::noop_coroutine(); - case task_state_t::awaited: // co_await-ed, resume parent + case state_t::awaited: // co_await-ed, resume parent return promise.parent; - case task_state_t::dangling: // task object is gone, free the handle + case state_t::dangling: // task object is gone, free the handle handle.destroy(); return std_coroutine::noop_coroutine(); - case task_state_t::done: // what + case state_t::done: // what // this should never happen. log it. we don't have a cluster so just write it on cerr std::cerr << "dpp::task: final_suspend called twice. something went very wrong here, please report to GitHub issues or the D++ Discord server" << std::endl; } @@ -684,23 +737,24 @@ std_coroutine::coroutine_handle<> detail::task_chain_final_awaiter::await_sus return std_coroutine::noop_coroutine(); } -} // namespace detail +} // namespace detail::task #ifndef _DOXYGEN_ inline void task::await_resume_impl() const { - if (handle.promise().exception) + if (handle.promise().exception) { std::rethrow_exception(handle.promise().exception); + } } #endif /* _DOXYGEN_ */ } // namespace dpp /** - * @brief Specialization of std::coroutine_traits, helps the standard library figure out a promise type from a coroutine function. + * @brief Specialization of std::coroutine_traits, helps the standard library figure out a promise_t type from a coroutine function. */ template struct dpp::detail::std_coroutine::coroutine_traits, Args...> { - using promise_type = dpp::detail::task_promise; + using promise_type = dpp::detail::task::promise_t; }; #endif /* DPP_CORO */ diff --git a/include/dpp/coro/when_any.h b/include/dpp/coro/when_any.h index a4ef3a73f3..96952c17e7 100644 --- a/include/dpp/coro/when_any.h +++ b/include/dpp/coro/when_any.h @@ -117,10 +117,10 @@ concept void_result = std::same_as; } // namespace detail -/** +/** @class when_any when_any.h coro/when_any.h * @brief Experimental class to co_await on a bunch of awaitable objects, resuming when the first one completes. - * On completion, returns a `result` object that contains the index of the awaitable that finished first. - * A user can call `get()` on the result object to get the result, similar to std::variant. + * On completion, returns a @ref result object that contains the index of the awaitable that finished first. + * A user can call @ref result::index() and @ref result::get() on the result object to get the result, similar to std::variant. * * @see when_any::result * @tparam Args... Type of each awaitable to await on @@ -188,45 +188,46 @@ class when_any { * @brief Spawn a dpp::job handling the Nth argument. * * @tparam N Index of the argument to handle + * @return dpp::job Job handling the Nth argument */ template static dpp::job make_job(std::shared_ptr shared_state) { /** - * Try co_await. Catch dpp::task_cancelled_exception, set cancelled bitmask, resume and rethrow if all tasks were cancelled. * Any exceptions from the awaitable's await_suspend should be thrown to the caller (the coroutine creating the when_any object) - * * If the co_await passes, and it is the first one to complete, try construct the result, catch any exceptions to rethrow at resumption, and resume. - * - * The structure of this function isn't ideal, but this is the best I've found do deal with concurrency and scope. */ if constexpr (!std::same_as, detail::when_any::empty>) { decltype(auto) result = co_await std::get(shared_state->awaitables); - if (auto s = shared_state->owner_state.load(std::memory_order_relaxed); s == detail::when_any::await_state::dangling || s == detail::when_any::await_state::done) + if (auto s = shared_state->owner_state.load(std::memory_order_relaxed); s == detail::when_any::await_state::dangling || s == detail::when_any::await_state::done) { co_return; + } using result_t = decltype(result); /* Try construct, prefer move if possible, store any exception to rethrow */ try { - if constexpr (std::is_lvalue_reference_v && !std::is_const_v && std::is_move_constructible_v>) + if constexpr (std::is_lvalue_reference_v && !std::is_const_v && std::is_move_constructible_v>) { shared_state->result.template emplace(std::move(result)); - else + } else { shared_state->result.template emplace(result); + } } catch (...) { shared_state->result.template emplace<0>(std::current_exception()); } } else { co_await std::get(shared_state->awaitables); - if (auto s = shared_state->owner_state.load(std::memory_order_relaxed); s == detail::when_any::await_state::dangling || s == detail::when_any::await_state::done) + if (auto s = shared_state->owner_state.load(std::memory_order_relaxed); s == detail::when_any::await_state::dangling || s == detail::when_any::await_state::done) { co_return; + } shared_state->result.template emplace(); } - if (shared_state->owner_state.exchange(detail::when_any::await_state::done) != detail::when_any::await_state::waiting) + if (shared_state->owner_state.exchange(detail::when_any::await_state::done) != detail::when_any::await_state::waiting) { co_return; + } if (auto handle = shared_state->handle; handle) { shared_state->index_finished = N; @@ -245,7 +246,7 @@ class when_any { public: /** - * @brief Object returned by `operator co_await` on resumption. Can be moved but not copied. + * @brief Object returned by \ref operator co_await() on resumption. Can be moved but not copied. */ class result { friend class when_any; @@ -285,16 +286,17 @@ class when_any { * @brief Retrieve the non-void result of an awaitable. * * @tparam N Index of the result to retrieve. Must correspond to index(). - * @throw Throws any exception triggered at construction, or std::bad_variant_access if N does not correspond to index() - * @return result_t& Result of the awaitable + * @throw ??? Throws any exception triggered at construction, or std::bad_variant_access if N does not correspond to index() + * @return Result of the awaitable as a reference. */ template #ifndef _DOXYGEN_ requires (!detail::when_any::void_result>) #endif result_t& get() & { - if (is_exception()) + if (is_exception()) { std::rethrow_exception(std::get<0>(shared_state->result)); + } return std::get(shared_state->result); } @@ -302,16 +304,17 @@ class when_any { * @brief Retrieve the non-void result of an awaitable. * * @tparam N Index of the result to retrieve. Must correspond to index(). - * @throw Throws any exception triggered at construction, or std::bad_variant_access if N does not correspond to index() - * @return const result_t& Result of the awaitable + * @throw ??? Throws any exception triggered at construction, or std::bad_variant_access if N does not correspond to index() + * @return Result of the awaitable as a cpnst reference. */ template #ifndef _DOXYGEN_ requires (!detail::when_any::void_result>) #endif const result_t& get() const& { - if (is_exception()) + if (is_exception()) { std::rethrow_exception(std::get<0>(shared_state->result)); + } return std::get(shared_state->result); } @@ -319,16 +322,17 @@ class when_any { * @brief Retrieve the non-void result of an awaitable. * * @tparam N Index of the result to retrieve. Must correspond to index(). - * @throw Throws any exception triggered at construction, or std::bad_variant_access if N does not correspond to index() - * @return result_t&& Result of the awaitable + * @throw ??? Throws any exception triggered at construction, or std::bad_variant_access if N does not correspond to index() + * @return Result of the awaitable as an rvalue reference. */ template #ifndef _DOXYGEN_ requires (!detail::when_any::void_result>) #endif result_t&& get() && { - if (is_exception()) + if (is_exception()) { std::rethrow_exception(std::get<0>(shared_state->result)); + } return std::get(shared_state->result); } @@ -343,14 +347,16 @@ class when_any { /** * @brief Checks whether the return of the first awaitable triggered an exception, that is, a call to get() will rethrow. + * + * @return Whether or not the result is an exception */ - bool is_exception() const noexcept { + [[nodiscard]] bool is_exception() const noexcept { return shared_state->result.index() == 0; } }; /** - * @brief Object returned by `operator co_await()`. Meant to be used by the standard library, not by a user. + * @brief Object returned by \ref operator co_await(). Meant to be used by the standard library, not by a user. * * @see result */ @@ -358,12 +364,20 @@ class when_any { /** @brief Pointer to the when_any object */ when_any *self; - /** @brief First function called by the standard library when using co_await. Returns true if we don't need to suspend */ - bool await_ready() const noexcept { - return self->is_ready(); + /** + * @brief First function called by the standard library when using co_await. + * + * @return bool Whether the result is ready + */ + [[nodiscard]] bool await_ready() const noexcept { + return self->await_ready(); } - /** @brief Second function called by the standard library when using co_await. Returns false if we want to resume immediately */ + /** + * @brief Second function called by the standard library when using co_await. + * + * @return bool Returns false if we want to resume immediately. + */ bool await_suspend(detail::std_coroutine::coroutine_handle<> caller) noexcept { auto sent = detail::when_any::await_state::started; self->my_state->handle = caller; @@ -386,8 +400,8 @@ class when_any { /** * @brief Constructor from awaitable objects. Each awaitable is executed immediately and the when_any object can then be co_await-ed later. * - * @throw Any exception thrown by the start of each awaitable will propagate to the caller. - * @param args... Arguments to construct each awaitable from. The when_any object will construct an awaitable for each, it is recommended to pass rvalues or std::move. + * @throw ??? Any exception thrown by the start of each awaitable will propagate to the caller. + * @param args Arguments to construct each awaitable from. The when_any object will construct an awaitable for each, it is recommended to pass rvalues or std::move. */ template #ifndef _DOXYGEN_ @@ -401,16 +415,19 @@ class when_any { when_any(const when_any &) = delete; /** @brief Move constructor. */ - when_any(when_any &&) = default; + when_any(when_any &&) noexcept = default; /** - * @brief On destruction the when_any will try to call `cancel()` on each of its awaitable if they have such a method. + * @brief On destruction the when_any will try to call @ref dpp::task::cancel() cancel() on each of its awaitable if they have such a method. * * @note If you are looking to use a custom type with when_any and want it to cancel on its destruction, * make sure it has a cancel() method, which will trigger an await_resume() throwing a dpp::task_cancelled_exception. * This object will swallow the exception and return cleanly. Any other exception will be thrown back to the resumer. */ ~when_any() { + if (!my_state) + return; + my_state->owner_state = detail::when_any::await_state::dangling; [](when_any *self, std::index_sequence) constexpr { @@ -427,18 +444,19 @@ class when_any { }(this, std::index_sequence_for()); } - /** This object is not copyable. */ + /** @brief This object is not copyable. */ when_any &operator=(const when_any &) = delete; - /** Move assignment operator. */ - when_any &operator=(when_any &&) = default; + /** @brief Move assignment operator. */ + when_any &operator=(when_any &&) noexcept = default; /** * @brief Check whether a call to co_await would suspend. * * @note This can change from false to true at any point, but not the other way around. + * @return bool Whether co_await would suspend */ - bool is_ready() const noexcept { + [[nodiscard]] bool await_ready() const noexcept { return my_state->owner_state == detail::when_any::await_state::done; } @@ -446,10 +464,10 @@ class when_any { * @brief Suspend the caller until any of the awaitables completes. * * @see result - * @throw On resumption, throws any exception caused by the construction of the result. + * @throw ??? On resumption, throws any exception caused by the construction of the result. * @return result On resumption, this object returns an object that allows to retrieve the index and result of the awaitable. */ - awaiter operator co_await() noexcept { + [[nodiscard]] awaiter operator co_await() noexcept { return {this}; } }; diff --git a/include/dpp/event_router.h b/include/dpp/event_router.h index 4000d49684..f6dd3b4664 100644 --- a/include/dpp/event_router.h +++ b/include/dpp/event_router.h @@ -121,16 +121,16 @@ class awaitable { /** * @brief Request cancellation. This will detach this object from the event router and resume the awaiter, which will be thrown dpp::task_cancelled::exception. * - * @throw As this resumes the coroutine, it may throw any exceptions at the caller. + * @throw ??? As this resumes the coroutine, it may throw any exceptions at the caller. */ void cancel(); /** * @brief First function called by the standard library when awaiting this object. Returns true if we need to suspend. * - * @return false always. + * @retval false always. */ - constexpr bool await_ready() const noexcept; + [[nodiscard]] constexpr bool await_ready() const noexcept; /** * @brief Second function called by the standard library when awaiting this object, after suspension. @@ -143,8 +143,8 @@ class awaitable { /** * @brief Third and final function called by the standard library, called when resuming the coroutine. * - * @throw dpp::task_cancelled_exception if cancel() has been called - * @return const T& Reference to the event that matched + * @throw @ref task_cancelled_exception if cancel() has been called + * @return const T& __Reference__ to the event that matched */ [[maybe_unused]] const T& await_resume(); }; @@ -161,25 +161,24 @@ typedef size_t event_handle; /** * @brief Handles routing of an event to multiple listeners. + * Multiple listeners may attach to the event_router_t by means of @ref operator()(F&&) "operator()". Passing a + * lambda into @ref operator()(F&&) "operator()" attaches to the event. * - * Multiple listeners may attach to the event_router_t by means of operator(). Passing a - * lambda into operator() attaches to the event. - * - * Dispatchers of the event may call the event_router_t::call() method to cause all listeners + * @details Dispatchers of the event may call the @ref call() method to cause all listeners * to receive the event. * - * The event_router_t::empty() method will return true if there are no listeners attached + * The @ref empty() method will return true if there are no listeners attached * to the event_router_t (this can be used to save time by not constructing objects that * nobody will ever see). * - * The event_router_t::detach() method removes an existing listener from the event, - * using the event_handle ID returned by operator(). + * The @ref detach() method removes an existing listener from the event, + * using the event_handle ID returned by @ref operator()(F&&) "operator()". * * This class is used by the library to route all websocket events to listening code. * * Example: * - * ```cpp + * @code{cpp} * // Declare an event that takes log_t as its parameter * event_router_t my_event; * @@ -195,7 +194,7 @@ typedef size_t event_handle; * * // Detach from an event using the handle returned by operator() * my_event.detach(id); - * ``` + * @endcode * * @tparam T type of single parameter passed to event lambda derived from event_dispatch_t */ @@ -346,7 +345,7 @@ template class event_router_t { /** * @brief Destructor. Will cancel any coroutine awaiting on events. * - * @throw Cancelling a coroutine will throw a dpp::task_cancelled_exception to it. + * @throw ! Cancelling a coroutine will throw a dpp::task_cancelled_exception to it. * This will be caught in this destructor, however, make sure no other exceptions are thrown in the coroutine after that or it will terminate. */ ~event_router_t() { @@ -355,7 +354,7 @@ template class event_router_t { // cancel all awaiters. here we cannot do the usual loop as we'd need to lock coro_mutex, and cancel() locks and modifies coro_awaiters try { coro_awaiters.back()->cancel(); - /** + /* * will resume coroutines and may throw ANY exception, including dpp::task_cancelled_exception cancel() throws at them. * we catch that one. for the rest, good luck :) * realistically the only way any other exception would pop up here is if someone catches dpp::task_cancelled_exception THEN throws another exception. @@ -394,21 +393,20 @@ template class event_router_t { /** * @brief Obtain an awaitable object that refers to an event with a certain condition. * It can be co_await-ed to wait for the next event that satisfies this condition. - * On resumption the awaiter will be given a reference to the event, + * On resumption the awaiter will be given __a reference__ to the event, * saving it in a variable is recommended to avoid variable lifetime issues. * - * Example - * ```cpp + * @details Example: @code{cpp} * dpp::job my_handler(dpp::slashcommand_t event) { * co_await event.co_reply(dpp::message().add_component(dpp::component().add_component().set_label("click me!").set_id("test"))); * - * button_click_t b = co_await c->on_button_click.with([](const button_click_t &event){ return event.custom_id == "test"; }); + * dpp::button_click_t b = co_await c->on_button_click.with([](const dpp::button_click_t &event){ return event.custom_id == "test"; }); * * // do something on button click * } - * ``` + * @endcode * - * This can be combined with `dpp::when_any` and other awaitables, for example `dpp::cluster::co_sleep` to create expiring buttons. + * This can be combined with dpp::when_any and other awaitables, for example dpp::cluster::co_sleep to create @ref expiring-buttons "expiring buttons". * * @warning On resumption the awaiter will be given a reference to the event. * This means that variable may become dangling at the next co_await, be careful and save it in a variable @@ -421,33 +419,37 @@ template class event_router_t { #ifndef _DOXYGEN_ requires utility::callable_returns #endif - auto when(Predicate&& pred) noexcept(noexcept(std::function{pred})) { - return detail::event_router::awaitable{this, pred}; + auto when(Predicate&& pred) +#ifndef _DOXYGEN_ + noexcept(noexcept(std::function{std::declval()})) +#endif + { + return detail::event_router::awaitable{this, std::forward(pred)}; } /** * @brief Obtain an awaitable object that refers to any event. * It can be co_await-ed to wait for the next event. * - * Example - * ```cpp + * Example: + * @details Example: @code{cpp} * dpp::job my_handler(dpp::slashcommand_t event) { * co_await event.co_reply(dpp::message().add_component(dpp::component().add_component().set_label("click me!").set_id("test"))); * - * button_click_t b = co_await c->on_message_create; + * dpp::button_click_t b = co_await c->on_message_create; * * // do something on button click * } - * ``` + * @endcode * - * This can be combined with `dpp::when_any` and other awaitables, for example `dpp::cluster::co_sleep` to create expiring buttons. + * This can be combined with dpp::when_any and other awaitables, for example dpp::cluster::co_sleep to create expiring buttons. * * @warning On resumption the awaiter will be given a reference to the event. * This means that variable may become dangling at the next co_await, be careful and save it in a variable * if you need to. * @return awaitable An awaitable object that can be co_await-ed to await an event matching the condition. */ - auto operator co_await() noexcept { + [[nodiscard]] auto operator co_await() noexcept { return detail::event_router::awaitable{this, nullptr}; } #endif @@ -456,20 +458,28 @@ template class event_router_t { * @brief Returns true if the container of listeners is empty, * i.e. there is nothing listening for this event right now. * - * @return true if there are no listeners - * @return false if there are some listeners + * @retval true if there are no listeners + * @retval false if there are some listeners */ - bool empty() const { - std::shared_lock l(mutex); + [[nodiscard]] bool empty() const { +#ifdef DPP_CORO + std::shared_lock lock{mutex}; + std::shared_lock coro_lock{coro_mutex}; + + return dispatch_container.empty() && coro_awaiters.empty(); +#else + std::shared_lock lock{mutex}; + return dispatch_container.empty(); +#endif } /** * @brief Returns true if any listeners are attached. * * This is the boolean opposite of event_router_t::empty(). - * @return true if listeners are attached - * @return false if no listeners are attached + * @retval true if listeners are attached + * @retval false if no listeners are attached */ operator bool() const { return !empty(); @@ -482,7 +492,7 @@ template class event_router_t { * `dpp::job(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 attach. + * This has the exact same behavior as using \ref attach(F&&) "attach". * * @see attach * @param fun Callable to attach to event @@ -490,7 +500,7 @@ template class event_router_t { * detach the listener from the event later if necessary. */ template - event_handle operator()(F&& fun); + [[maybe_unused]] event_handle operator()(F&& fun); /** * @brief Attach a callable to the event, adding a listener. @@ -503,7 +513,7 @@ template class event_router_t { * detach the listener from the event later if necessary. */ template - event_handle attach(F&& fun); + [[maybe_unused]] event_handle attach(F&& fun); #else /* not _DOXYGEN_ */ # ifdef DPP_CORO /** @@ -517,7 +527,7 @@ template class event_router_t { */ template requires (utility::callable_returns || utility::callable_returns) - event_handle operator()(F&& fun) { + [[maybe_unused]] event_handle operator()(F&& fun) { return this->attach(std::forward(fun)); } @@ -532,7 +542,7 @@ template class event_router_t { */ template requires (utility::callable_returns || utility::callable_returns) - event_handle attach(F&& fun) { + [[maybe_unused]] event_handle attach(F&& fun) { std::unique_lock l(mutex); event_handle h = next_handle++; dispatch_container.emplace(h, std::forward(fun)); @@ -549,7 +559,7 @@ template class event_router_t { * detach the listener from the event later if necessary. */ template - std::enable_if_t, event_handle> operator()(F&& fun) { + [[maybe_unused]] std::enable_if_t, event_handle> operator()(F&& fun) { return this->attach(std::forward(fun)); } @@ -558,12 +568,14 @@ template class event_router_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. + * * @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 - std::enable_if_t, event_handle> attach(F&& fun) { + [[maybe_unused]] std::enable_if_t, event_handle> attach(F&& fun) { std::unique_lock l(mutex); event_handle h = next_handle++; dispatch_container.emplace(h, std::forward(fun)); @@ -574,11 +586,13 @@ template class event_router_t { /** * @brief Detach a listener from the event using a previously obtained ID. * - * @param handle An ID obtained from event_router_t::operator() - * @return true The event was successfully detached - * @return false The ID is invalid (possibly already detached, or does not exist) + * @warning You cannot call this within an event handler. + * + * @param handle An ID obtained from @ref operator(F&&) "operator()" + * @retval true The event was successfully detached + * @retval false The ID is invalid (possibly already detached, or does not exist) */ - bool detach(const event_handle& handle) { + [[maybe_unused]] bool detach(const event_handle& handle) { std::unique_lock l(mutex); return this->dispatch_container.erase(handle); } @@ -591,7 +605,6 @@ namespace detail::event_router { template void awaitable::cancel() { awaiter_state s = awaiter_state::waiting; - /** * If state == none (was never awaited), do nothing * If state == waiting, prevent resumption, resume on our end @@ -619,8 +632,9 @@ template const T &awaitable::await_resume() { handle = nullptr; predicate = nullptr; - if (state.exchange(awaiter_state::none, std::memory_order_relaxed) == awaiter_state::cancelling) + if (state.exchange(awaiter_state::none, std::memory_order_relaxed) == awaiter_state::cancelling) { throw dpp::task_cancelled_exception{"event_router::awaitable was cancelled"}; + } return *std::exchange(event, nullptr); }