mirror of
https://github.com/HappyTanuki/BumbleCee.git
synced 2025-12-18 05:03:28 +00:00
스킵 구현
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
\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.
|
||||
|
||||
\include{cpp} coro_awaiting_events.cpp
|
||||
|
||||
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
|
||||
@@ -0,0 +1,20 @@
|
||||
\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:
|
||||
|
||||
|
||||
\include{cpp} coro_intro.cpp
|
||||
|
||||
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::task coroutine attached to them, as this object allows to create coroutines that can execute on their own, asynchronously. More on that and the difference between it and the other two types later. To turn a function into a coroutine, simply make it return dpp::task<void> as seen in the example at line 10, then use `co_await` on awaitable types or `co_return`. The moment the execution encounters one of these two keywords, the function is transformed into a coroutine. Coroutines that use dpp::task<void> 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.
|
||||
@@ -0,0 +1,21 @@
|
||||
\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 :
|
||||
|
||||
\include{cpp} coro_simple_commands1.cpp
|
||||
|
||||
### 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:
|
||||
|
||||
\include{cpp} coro_simple_commands2.cpp
|
||||
@@ -0,0 +1,11 @@
|
||||
\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.
|
||||
|
||||
\include{cpp} coro_expiring_buttons.cpp
|
||||
|
||||
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 <a href="https://www.learncpp.com/cpp-tutorial/value-categories-lvalues-and-rvalues/">temporary objects (rvalues)</a> as most of the coroutine-related objects in D++ are move-only.
|
||||
Reference in New Issue
Block a user