C++20 Coroutine Improvements in Visual Studio 2019 version 16.11
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.
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.
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.
/awaitswitch predates not only our
/std:c++20switches 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
/awaitenables 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
/await:strictis the latter does not define the spaceship operator for
std::coroutine_handle. Instead, it defines individual relational operators.
/await:strictmay 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
stdnamespace, so your code will be drop-in ready for C++20. Code compiled with
/await:strictuses the same coroutine ABI as
/std:c++latest, so coroutine objects are compatible between the two modes.
/awaitto 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
/awaitswitch at some point in the future.
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.
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.