'Coroutine awaiter methods are called for seemingly not-existing object

In the following program there are two coroutines. coroutine_A is empty and simply calls co_return, and coroutine_B just switches to the first coroutine once by calling co_await.

Also the program prints every time a task object is created or destructed and each time the methods await_ready and await_suspend are called:

#include <coroutine>
#include <iostream>

struct task {
    struct promise_type;
    
    using handle_type = std::coroutine_handle<promise_type>;

    task(handle_type h) : handle(h) {
        std::cout << "task ctor " << this << '\n';
    }
    ~task() {
        std::cout << "task dtor " << this << '\n';
    }

    struct promise_type {
        auto get_return_object() {
            return task{handle_type::from_promise(*this)};
        }

        auto initial_suspend() {
            return std::suspend_always {};
        }

        auto unhandled_exception() {}

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

        void return_void() {}
    };

   handle_type handle;

   void await_resume() {
       std::cout << "await_resume " << this << '\n';
       handle.resume();
   }

   auto await_suspend(handle_type) {
       std::cout << "await_suspend " << this << '\n';
       return handle;
   }

   auto await_ready() {
       std::cout << "await_ready " << this << '\n';
       return false;
   }
};

int main() {
  task coroutine_A = []() ->task {
    co_return;
  }();

  task coroutine_B = [&coroutine_A]() ->task {
    co_await coroutine_A;
  }();

  coroutine_B.handle.resume();
}

What surprises me is that the program built by GCC prints here, for example:

task ctor 0x7ffdef0f9458
task ctor 0x7ffdef0f9450
await_ready 0x431f30
await_suspend 0x431f30
task dtor 0x7ffdef0f9450
task dtor 0x7ffdef0f9458

Please note that this inside task::await_ready and task::await_suspendis not equal to any constructed instance of task. Demo: https://gcc.godbolt.org/z/WPnGqdhjr

Is it a GCC bug or there is some other explanation?



Solution 1:[1]

First, your example exhibit undefined behaviour. Let's look at this:

task coroutine_B = [&coroutine_A]() ->task {
    co_await coroutine_A;
}();

coroutine_B.handle.resume();

Your task::promise_type suspends in initial suspend so line co_await coroutine_A; is invoked only after coroutine_B.handle.resume();. But your lambda is already destroyed by that time. So it's capture (reference to coroutine_A) is destroyed as well.

If we add task copy constructor to your original code we will see that it is called and await_ready and await_suspend are called from that copy:

task ctor 0x7ffff491d1f8
task ctor 0x7ffff491d1f0
task copy to 0x1734f30
await_ready 0x1734f30
await_suspend 0x1734f30
task dtor 0x7ffff491d1f0
task dtor 0x7ffff491d1f8

Initially I was surprised that there were no corresponding destructor call of that newly created task. But your task class never call handle.destroy() and task::promise_type suspends in final suspend. So all objects alive by the end of the coroutine are never destroyed.

After fixing that undefined behaviour there is no copy at all:

task ctor 0x7ffeee8b8860
task ctor 0x7ffeee8b8858
await_ready 0x7ffeee8b8860
await_suspend 0x7ffeee8b8860
task dtor 0x7ffeee8b8858
task dtor 0x7ffeee8b8860

Unfortunately I don't know whether a copy of task is allowed in the co_await coroutine_A; as evaluation of an await-expression is not clear enough for me to find it out.

Please note that this inside task::await_ready and task::await_suspendis not equal to any constructed instance of task. Is it a GCC bug or there is some other explanation?

That task is created by copy constructor. As in your example task does not have user provided copy constructor with logging that was not obvious from program output. Unfortunately your example exhibit undefined behaviour.

After fixing undefined behaviour gcc version of the example makes no copies too. Maybe that coping was related to having UB in the program.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1