This post includes contributions from Terry Mahaffey and Ramkumar Ramesh.
We last blogged about coroutine support in Visual Studio 2019 version 16.8. In the releases since 16.8 we’ve introduced several new coroutine features and improvements. This post is a round up of those improvements, all available in Visual Studio 2019 16.11.
Debugging Improvements
Since Visual Studio 2019 version 16.9, stepping into a coroutine call will now land directly in the coroutine body (unless it is set to initially suspend, in which case the step becomes a “step over”). Stepping over a co_await
will land in the logical statement following co_await
for the coroutine – which may be in a completely different execution context (even another thread)! This allows stepping through coroutines to seamlessly match the logical flow of the application and skip intermediate implementation details. For the best debugging experience, implementation details of the coroutine state should be marked as non-user code. Stepping through coroutines now also shows function parameters as expected in the Locals window so you can see the state of the application, similar to stepping through synchronous functions.
Inspecting the state of a suspended coroutine is now easier with some improvements to the debugging visualizers for standard coroutines. The legacy coroutine_handle
visualizers could display special indicators for the initial and final suspend points, but only showed a number for other suspend points. This number was not always easy to map back to a particular point in the original coroutine. The visualizer also showed the name of the coroutine but only as a modified, internal name generated by the implementation with no signature information.
With the new coroutine handle visualizer introduced in Visual Studio 2019 16.10 the function name is now correct and includes full signature information to help distinguish overloaded coroutines. The suspend point information for suspend points other than initial and final suspend also includes the source line number to make it easier to find.
/await:strict
The earlier blog post outlines some issues with legacy await mode and the rationale for keeping the /await
switch distinct from C++20 coroutine support in /std:c++latest
. Legacy mode is useful for users who were early adopters of C++ coroutines, but they are not standard coroutines.
/await
switch predates not only our /std:c++latest
and /std:c++20
switches but also /std:c++17
. Early adopters were able to make use of coroutines long before they became part of the C++ standard. These users could use coroutines without requiring their code to be C++20 conformant or even necessarily C++17 conformant. With standard coroutines available only under C++20 and latest modes, early adopters of coroutines who cannot move their code to a more recent language version were stuck with the legacy implementation of coroutines under/await
. They could not take advantage of some new features like symmetric transfer and improved debugger support, even if they were willing to make source changes to the coroutines themselves to bring them in line with the C++20 standard./await:strict
. Using this switch instead of /await
enables the same C++20 coroutine support as standard mode but without all the other requirements of /std:c++20
. This includes support for all standard C++20 coroutine features and debugger integration and disables all the legacy extensions still supported under /await
. The only difference between /std:c++20
coroutines and /await:strict
is the latter does not define the spaceship operator for std::coroutine_handle
. Instead, it defines individual relational operators./await
to /await:strict
may require source changes if your code relies on extensions that were not adopted into C++20. Like Standard mode it uses the <coroutine>
header and the std
namespace, so your code will be drop-in ready for C++20. Code compiled with /await:strict
uses the same coroutine ABI as /std:c++latest
, so coroutine objects are compatible between the two modes./await
to migrate to /await:strict
. You can take advantage of all new coroutine features as well as ensure your coroutine code is ready for C++20 when you can move to a C++ language version that officially supports coroutines. We expect to deprecate and remove the /await
switch at some point in the future.Stability Improvements
Visual Studio 2019 version 16.11 also includes several important fixes to improve the stability and reliability of coroutines.
The largest change relates to how the optimizer does what is called “promotion”, which is the algorithm to decide which variables get placed on the coroutine frame and which variables remain on the (traditional) stack. Many coroutine bugs can be traced back to an incorrect decision here. Typically this shows up as a crash, or as a variable having an incorrect or random value after a coroutine resumes execution. This promotion algorithm has been rewritten to be more accurate, and the result is less crashes and a much smaller coroutine frame size overall. The old algorithm is still accessible by passing /d2CoroNewPromotion-
to cl.exe.
A related fix concerns how exception objects are stored. The lifetime rules for exceptions can get complicated, and they need to be handled specifically when it comes time to decide variable promotion.
A bug was found and fixed related to catch blocks in coroutines. Under certain circumstances (namely, when the only throwing call in a try block was from a user defined awaiter method) the optimizer could erroneously conclude a catch block was dead, and incorrectly remove it. The compiler is now aware that awaiter methods can throw.
Finally, a serious issue was resolved related to how and when destructors are invoked. This relates to how the construction state is tracked in coroutines for certain objects which are conditionally destroyed when leaving a scope. It comes up most when constructing objects when using the conditional (ternary) operator. The bug manifests itself by a destructor for such temporary objects not being invoked, or in certain cases invoked twice. This has also been fixed in 16.11.
Feedback
We urge you to try out C++ coroutines in Visual Studio, either with C++20 or now with /await:strict
, to see how asynchronous functions can help make your code more natural. As always, we welcome feedback on our coroutine implementation either in the comments below, or for bug reports and feature requests directly on Developer Community.
Great stuff! Though I’m particularly interested in heap allocation elision. Has there been made any progress since 16.8? I couldn’t get the compiler to avoid coroutine frame heap allocation with even the simplest of coroutines.
Hey Sylvester! Short answer, no: performance improvements like fully productizing heap elision is on our backlog, but we have chosen to focus on stability and correctness improvements first. We’ll keep you updated!
Understandable. I can’t wait to start using coroutines for my job system, but the pretty hefty alloc for each coroutine is currently holding me back. That being said, of course stability and correctness is even more important 😁.