Debugging C++ Coroutines#

Introduction#

Coroutines in C++ were introduced in C++20, and the user experience for debugging them can still be challenging. This document guides you on how to most efficiently debug coroutines and how to navigate existing shortcomings in debuggers and compilers.

Coroutines are generally used either as generators or for asynchronous programming. In this document, we will discuss both use cases. Even if you are using coroutines for asynchronous programming, you should still read the generators section, as it introduces foundational debugging techniques also applicable to the debugging of asynchronous programs.

Both compilers (clang, gcc, …) and debuggers (lldb, gdb, …) are still improving their support for coroutines. As such, we recommend using the latest available version of your toolchain.

This document focuses on clang and lldb. The screenshots show lldb-dap in combination with VS Code. The same techniques can also be used in other IDEs.

Debugging clang-compiled binaries with gdb is possible, but requires more scripting. This guide comes with a basic GDB script for coroutine debugging.

This guide will first showcase the more polished, bleeding-edge experience, but will also show you how to debug coroutines with older toolchains. In general, the older your toolchain, the deeper you will have to dive into the implementation details of coroutines (such as their ABI). The further down you go in this document, the more low-level, technical the content will become. If you are on an up-to-date toolchain, you will hopefully be able to stop reading earlier.

Debugging generators#

One of the two major use cases for coroutines in C++ is generators, i.e., functions which can produce values via co_yield. Values are produced lazily, on-demand. For this purpose, every time a new value is requested, the coroutine gets resumed. As soon as it reaches a co_yield and thereby returns the requested value, the coroutine is suspended again.

This logic is encapsulated in a generator type similar to this one:

// generator.hpp
#include <coroutine>

// `generator` is a stripped down, minimal generator type.
template<typename T>
struct generator {
  struct promise_type {
    T current_value{};

    auto get_return_object() {
      return std::coroutine_handle<promise_type>::from_promise(*this);
    }
    auto initial_suspend() { return std::suspend_always(); }
    auto final_suspend() noexcept { return std::suspend_always(); }
    auto return_void() { return std::suspend_always(); }
    void unhandled_exception() { __builtin_unreachable(); }
    auto yield_value(T v) {
      current_value = v;
      return std::suspend_always();
    }