Search code examples
c++memory-leaksc++-coroutine

Memory leak when returning `std::suspend_always` from `promise_type::final_suspend`


I created the following simple coroutine example:

#include <cassert>   // std::assert
#include <coroutine> // std::suspend_never, suspend_always
#include <cstdio>    // printf

class test {
public:
  class promise_type {
  public:
    explicit promise_type() {}

    void get_return_object() const {}

    std::suspend_never initial_suspend() const { return {}; }

    void unhandled_exception() const { assert(false); }

    void return_void() const {}

    std::suspend_always final_suspend() const noexcept { return {}; }
  };
};

test process() {
  co_return;
}

int main() {
  process();
}

Compiling this with GCC (with -fcoroutines) or Clang and then running the executable with Valgrind produces the following output:

==135311== Memcheck, a memory error detector
==135311== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==135311== Using Valgrind-3.20.0 and LibVEX; rerun with -h for copyright info
==135311== Command: test
==135311== 
==135311== 
==135311== HEAP SUMMARY:
==135311==     in use at exit: 40 bytes in 1 blocks
==135311==   total heap usage: 2 allocs, 1 frees, 72,744 bytes allocated
==135311== 
==135311== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==135311==    at 0x4842003: operator new(unsigned long) (vg_replace_malloc.c:434)
==135311==    by 0x1091BE: process() (test.cpp:25)
==135311==    by 0x109466: main (test.cpp:28)
==135311== 
==135311== LEAK SUMMARY:
==135311==    definitely lost: 40 bytes in 1 blocks
==135311==    indirectly lost: 0 bytes in 0 blocks
==135311==      possibly lost: 0 bytes in 0 blocks
==135311==    still reachable: 0 bytes in 0 blocks
==135311==         suppressed: 0 bytes in 0 blocks
==135311== 
==135311== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

This memory leak is not reported when returning std::suspend_never from promise_type::final_suspend. However, according to https://en.cppreference.com/w/cpp/language/coroutines and many other sources, resuming a coroutine after promise_type::final_suspend has been called is undefined behavior. Is this memory leak expected or can I do something else to prevent it?


Solution

  • It is the responsibility of your coroutine machinery to actually clean up the coroutine when your machinery is finished with it. In typical cases, test would store a coroutine_handle which it would dutifully destroy in its destructor. In other cases, handles are given out to other code which itself is responsible for destroying them.

    In your case, you would need the promise to destroy the handle somewhere in final_suspend.

    Put simply, ownership over the coroutine_handle is your code's responsibility. Coroutines cease to exist only when you make them.