Should the coroutine result object ever be constructed after initial_suspend()?

Viewed 121

I've been writing a coroutine library, and I've run into a peculiar problem. In some cases, the construction of the coroutine result object was sequenced after the call to initial_suspend.

Question: is this sequencing a bug on the part of the compiler?

Background

In my particular case, this was causing a crash, as a generator promise executed under the assumption that it had no owner.

The relevant section of the C++20 standard is section 9.5.4.7, which states:

The expression promise.get_return_object() is used to initialize the glvalue result or prvalue result object of a call to a coroutine. The call to get_return_object is sequenced before the call to initial_suspend and is invoked at most once.

When I read this portion of the standard, I originally interpreted it to mean that the initialization of the coroutine's result object is sequenced immediately after the call to promise.get_return_object(): when you initialize something, initialization occurs immediately after the computation of the arguments to the constructor, and not as a delayed effect.

Unfortunately, this is not the behavior I observed. The source of the crash is that the initialization of the result object was sequenced after the initial suspend, despite the call to promise.get_return_object() being sequenced before the initial suspend.

Observing this behavior in code (view live example here)

Let's write a very simple type that can be returned from a coroutine:

template <class Promise>
struct coroutine {
    std::coroutine_handle<Promise> handle;
    using promise_type = Promise;
    coroutine(std::coroutine_handle<Promise> p) : handle(p) {
        std::cout << "  Running coroutine(std::coroutine_handle<Promise> p)\n";
    }

    ~coroutine() {
        if (handle) {
            handle.destroy();
        }
    }
};

Because coroutine<Promise> can be constructed from a std::coroutine_handle<Promise>, the call to promise.get_return_object() can return either a std::coroutine_handle<Promise> which is used to construct the coroutine, or it can return a coroutine<Promise> directly.

Let's write two different promise types, one for each option:

struct promise_base {
    std::suspend_never initial_suspend() {
        std::cout << "  Running initial_suspend()\n";
        return {};
    }
    std::suspend_always final_suspend() { return {}; }
    void return_void() {}
    void unhandled_exception() { std::terminate(); }
};

struct promise_A : promise_base {
    using handle = std::coroutine_handle<promise_A>;
    handle get_return_object() {
        std::cout << "  Running get_return_object()\n";
        return handle::from_promise(*this);
    }
};
struct promise_B : promise_base {
    using handle = std::coroutine_handle<promise_B>;
    coroutine<promise_B> get_return_object() {
        std::cout << "  Running get_return_object()\n";
        return {handle::from_promise(*this)};
    }
};

We can then write two identical coroutines based on promise_A and promise_B:

coroutine<promise_A> run_A() {
    std::cout << "Inside coroutine body\n";
    co_return;
}
coroutine<promise_B> run_B() {
    std::cout << "Inside coroutine body\n";
    co_return;
}

In this code,

  • promise_A returns a handle from get_return_object, and this is used to construct a result object of type coroutine<promise_A>.
  • promise_B directly returns a coroutine<promise_B>, which is returned as the result object.

In the case of promise_A, the construction of the coroutine<promise_A> result object is sequenced after the call to initial_suspend().

We can write the following test code to verify this:

void test_promise_A() {
    std::cout << "------ Testing A ------\n";
    run_A();
    std::cout << "\n\n";
}
void test_promise_B() {
    std::cout << "------ Testing B ------\n";
    run_B();
    std::cout << "\n\n";
}
int main() {
    test_promise_A();
    test_promise_B();
}

Under GCC 10.1, this produces the following output. Note the difference in sequencing between Testing A and Testing B.

------ Testing A ------
  Running get_return_object()
  Running initial_suspend()
Inside coroutine body
  Running coroutine(std::coroutine_handle<Promise> p)


------ Testing B ------
  Running get_return_object()
  Running coroutine(std::coroutine_handle<Promise> p)
  Running initial_suspend()
Inside coroutine body

To reiterate the question: Is the sequencing of Testing A a bug?

0 Answers
Related