Skip to content

CWG2935 [dcl.fct.def.coroutine] Destroying the coroutine state when initial-await-resume-called is false #575

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
t3nsor opened this issue Jul 13, 2024 · 6 comments

Comments

@t3nsor
Copy link

t3nsor commented Jul 13, 2024

Full name of submitter: Brian Bi

Reference (section label): [dcl.fct.def.coroutine]

Issue description: According to [dcl.fct.def.coroutine]/11, the coroutine state is destroyed only when "control flows off the end" or when .destroy() is called on a coroutine handle.

In [dcl.fct.def.coroutine]/5, suppose that an exception occurs at any point before initial-await-resume-called becomes true. Then [dcl.fct.def.coroutine]/11 does not state that the coroutine state is destroyed, nor does anything in this section state that the coroutine is suspended. Consequently, the caller is not able to destroy the coroutine state either and the coroutine state is leaked.

If the intent is to leak the coroutine state but still give the caller some means of observing the exception (e.g. log it and then terminate), this should be clarified in a note; otherwise, the implementation should either destroy the coroutine state or suspend the coroutine.

@t3nsor
Copy link
Author

t3nsor commented Sep 9, 2024

Bump

@jensmaurer
Copy link
Member

CWG2935

@jensmaurer jensmaurer changed the title [dcl.fct.def.coroutine] Destroying the coroutine state when initial-await-resume-called is false CWG2935 [dcl.fct.def.coroutine] Destroying the coroutine state when initial-await-resume-called is false Sep 9, 2024
@YexuanXiao
Copy link

The current wording originates from N4842, which was created when merging P0664R8. I believe the leak was unintentional.

@t3nsor
Copy link
Author

t3nsor commented Nov 17, 2024

The current wording originates from N4842, which was created when merging P0664R8. I believe the leak was unintentional.

Do you know the exact behavior that's intended? If I remember correctly, the last time I looked at this, I had some difficulty in constructing a specification that would be satisfactory for all use cases.

@YexuanXiao
Copy link

YexuanXiao commented Nov 18, 2024

I believe the true intention of P0664R8 is that exceptions thrown before the initial suspend point will propagate to the coroutine's caller, while exceptions thrown after the initial suspend point can be caught in unhandled_exception.

I wrote a simple test where the exception is thrown in await_ready. Surprisingly, the three major implementations gave different results (Compiler Explorer):

  • All three destruct the initial_awaiter:
    • GCC did the least: it only destructs initial_awaiter (seemingly placing the coroutine in the final suspend point but leaking the coroutine).
    • Clang did more: it additionally destructs the promise (quite odd).
    • MSVC (and older GCC 14.2) did the most: it also destructs task before promise. However, automatically destructing promise causes a double delete if the destructor of task destroys the coroutine.

All three implementations have flaws.

The key point is that ignoring the destruction of task is wrong regardless. task typically owns the coroutine, it needs to destruct the promise (through destroy), at the same time, destruct its own members.

The solution I propose is:

  • If an exception is thrown before the coroutine return value is fully constructed (if task invokes the move constructor before fully constructed, then the move constructor should not throw exceptions), destruct the promise, function arguments, the coroutine state, and propagate the exception to the caller.
  • If an exception is thrown before the initial suspend point, destruct initial_awaiter, set the state at final suspend point (like the solution of CWG 2451), destruct the coroutine return value, and propagate the exception to the caller.

UPDATE:

The issue is essentially a duplicate of CWG 2562, and related issue CWG 2563 raises additional concerns about the details of get_return_object.

Currently, the standard does not specify when get_return_object should be called and the lifetime of the returned object. Therefore, these three issues should be resolved together.

The proposed additions are:

  • Calling get_return_object should be before and outside the try-block in #5.
  • As if task{ promise.get_return_object() }, direct-initialized the coroutine's return value.

The latter will allow get_return_object to return promise_type&, ensuring that the task is only constructed in place once, thereby avoiding the issue of the move constructor throwing an exception.

@Mishura4
Copy link

Mishura4 commented Apr 6, 2025

Bump; CWG 2934 (#574) is also related

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants