C++ Coroutines in Visual Studio 2019 Version 16.8

Avatar

Jonathan

Please see our Visual Studio 2019 version 16.8 Preview 3 release notes for more of our latest features.

It’s been a long journey for coroutines in C++ and in MSVC. We announced an early preview of resumable functions in 2013, followed up by the /await switch and initial C++ standardization proposals in 2014, to proposal revisions in 2015, and have continued tracking the Coroutines TS (Technical Specification) progress through Visual Studio 2017 and 2019. With the adoption of coroutines into the C++ standard in 2019, we are now pleased to announce feature completion of C++20 coroutines in Visual Studio 2019 version 16.8.

Standard vs. TS Coroutines

The coroutine support that ultimately made it through the standardization process and became part of C++20 is different from the early proposal drafts and from experimental coroutine support we’ve had in MSVC under the /await switch. This led us to two important and contradictory goals in finishing the coroutine language support in 16.8:

  1. Provide an implementation of C++20 coroutines that strictly follows the standard, allowing users to write and consume portable code.
  2. Ensure existing users of experimental coroutines can painlessly upgrade to 16.8 without needing to change their code.

As the proposal changed, we’ve added new support whenever possible without breaking existing code for early adopters of coroutines. This is of course not standard: It still accepts all the old keywords, names, and signatures, counter to goal 1. There are also a small number of behavior changes from the original versions we implemented under /await, such as how a promise object is constructed. These could cause a program that previously compiled to fail to compile or behave differently at runtime.

Standard Mode – /std:c++latest

Support for C++20 coroutines without legacy TS support is now enabled when using a compiler language version mode newer than C++17. For now, this is /std:c++latest and will continue into numbered version switches after C++17 as these are added. When compiling with such a language switch and without /await you get strict support for C++20 coroutines with library support in the <coroutine> header and defined in the std namespace. This mode will emit errors on non-standard code from earlier proposals, such as a bare await keywords or an initial_suspend function that returns bool, and only supports the standard behaviors when they differ from earlier implementations.

Extension Mode – /await

Early adopters of coroutines can continue to compile their non-standard code with the /await switch and any of the language version switches (including /std:c++latest), and continue to use the experimental headers and namespace. We have added missing standard features and bug fixes in this mode as long as they don’t break compatibility.

We recommend existing coroutine users move to standard coroutines as soon as possible, and new users should favor the standard mode over /await.  Support for the /await switch will continue for existing users, but the future of coroutines is in the standard mode and new features will be implemented there. Excepting some corner cases migrating a project from /await to C++20 is a straightforward process.

What’s New in 16.8

Version 16.8 introduces several new features and improvements in coroutines:

  • Symmetric transfer
  • No-op coroutines
  • Coroutine promise constructor parameters
  • Well-defined behavior for exceptions leaving a coroutine body
  • Standard return object conversion behavior
  • Improved debugging experience
  • Common frame layout for improved compatibility with other vendors
  • Numerous bug fixes

Most of these changes are available only when building in standard mode, although no-op coroutines and most bug fixes have also been implemented under /await. In the rest of this post we’ll take a closer look at some of these items and what’s next for coroutines in Visual Studio.

Symmetric transfer and no-op coroutines

These were the last two big missing pieces for C++20 coroutine support. With symmetric transfer a coroutine can indicate a coroutine handle for another coroutine to immediately resume when suspending. This is done by defining the await_suspend function of the coroutine promise with a return type of coroutine_handle<T>:

struct some_awaitable {
  ...
  std::coroutine_handle<> await_suspend(std::coroutine_handle<promise_type> h) noexcept {
    // If the coroutine that is about to suspend (indicated by h) has a continuation
    // coroutine handle, resume that coroutine instead of returning to the caller.
    // Otherwise, return a no-op coroutine. The no-op coroutine does nothing, and will
    // allow control to return to the caller.
    return h.promise().continuation ? *continuation : std::noop_coroutine();
  }
};

In standard mode this suspend-and-resume operation works without introducing another frame onto the call stack. This allows an unlimited number of transfers between coroutines without risking a stack overflow.

Improved debugging experience

Version 16.8 introduces several new debugging features for working with coroutines. Some issues with stepping into and within coroutines have been fixed, especially with Just My Code. It’s also now possible to expand the frame pointer while within a coroutine. This exposes data like coroutine parameter values and members of the promise type (standard coroutines only). We’ve also changed the names of many compiler-generated symbols to work better with the debugger’s expression evaluation. These are now easier to use in an immediate or watch window, or as a conditional breakpoint.

Conditional breakpoints and frame layout

Common frame layout

There is a new internal representation of a coroutine frame in standard C++20 mode. This exposes the parts of the frame that are important for working with a coroutine, such as how to resume or destroy it, in a way common across vendors. Coroutines produced in an object file or library produced by one vendor can then potentially be used by another. This doesn’t mean that the full frame layout is common across vendors or even guaranteed to be stable across compiler versions, but it does standardize (although unofficially) the interface between the standard library type std::coroutine_handle and the underlying coroutine frame object, and should help improve compatibility and flexibility when exposing or consuming a coroutine from a library. We’ve also introduced support for the same builtin functions used by Clang, allowing for better header-level compatibility.

The level of coroutine support among different vendors currently varies, but is improving. As C++20 support rolls out widely across compilers we expect this to become more useful and important. We’re committed to providing a common, stable ABI for coroutines to make interfacing between different builds as seamless as possible.

What’s Next?

Coroutines in C++20 are a bit limited. The core language feature has been adopted, but there is no real coroutine support in the standard library. The good news is we expect to change relatively soon, with more extensive library support for coroutines in the next C++ language version.

Our next steps for C++20 coroutines are in continued improvement of the debugging experience. One aspect of this is more natural stepping behavior, making it easier to trace through a coroutine execution as if it was a normal, synchronous function. We’re also looking at improved visualization of coroutine handles to easily see the state of a suspended coroutine.

As always, feedback on this feature is welcome, and bug reports can be made on Developer Community. Happy co_awaiting!

4 comments

Leave a comment

  • Avatar
    Christoph Hausner

    Great to see full coroutine support, had been waiting for symmetric transfer to be supported.

    For anyone who wants to get started with coroutines, I can highly recommend the cppcoro library (although the library hasn’t been updated yet for the latest VS preview versions so you might need to workaround some build issues).

    • Avatar
      Jonathan EmmettMicrosoft employee

      Hi Christoph,

      I agree on recommending cppcoro, and we have been testing with this project internally as well. The main branch still targets VS2017 but there is a vs2019 branch with the required changes to build with VS 2019, with a few caveats:

      – It likely will not enable symmetric transfer support yet, since that’s new to 16.8.
      – There is a known issue with one of the tests affecting x86, optimized, LTCG builds only where an exception is not caught at the correct place. This is a MSVC bug that we are tracking.
      – There is another potential issue in another test that may be a race condition in cppcoro itself, see issue 114.

  • Avatar
    Matteo Amato

    I have been using the experimental coroutine support for some time, with the /d2CoroOptsWorkaround switch enabled on certain libraries due to internal compiler errors. Having transitioned to standard coroutines, I’m still receiving ICEs when compiling with optimizations enabled, in different places than before. Should we still be making liberal use of /d2CoroOptsWorkaround for the time being when using coroutines?

    (I’m wary of posting to the forum as I can’t provide a minimal repro – the codebase makes liberal use of chained tasks, and I can see nothing in particular about the areas reported in the ICEs that should cause a problem. Is any of the data from the ICE message worth reporting to the forum by itself?)

    • Avatar
      Jonathan EmmettMicrosoft employee

      Hi Matteo,

      Yes the optimizer team still recommends using /d2CoroOptsWorkaround if you are having trouble with optimizations in coroutines. Even if you can’t provide a minimal repro, if you are able to share the code they would appreciate even a large repro case that has optimization issues.