3582. Unclear where std::async exceptions are handled

Section: 32.10.9 [futures.async] Status: New Submitter: Jonathan Wakely Opened: 2021-08-23 Last modified: 2021-09-30

Priority: 3

View other active issues in [futures.async].

View all other issues in [futures.async].

View all issues with New status.

Discussion:

32.10.9 [futures.async] (3.1) says:

Any exception propagated from the execution of invoke(decay-copy(std::forward<F>(f)), decay-copy(std::forward<Args>(args))...) is stored as the exceptional result in the shared state.

It's not clear whether this includes the evaluation of the decay-copy calls in the calling thread, or only the invocation of invoke with the results of those decay-copy calls.

A literal reading suggests that any exceptions from any part of that expression should be stored in the shared state. All of libstdc++, libc++ and MSVC only store exceptions from the call to invoke, not the calls to decay-copy. Exceptions from the decay-copy calls are propagated to the caller of std::async. We should clarify that that's what the standard means.

[2021-09-20; Reflector poll]

Set priority to 3 after reflector poll.

[2021-09-20; Jonathan updates wording to change the Throws: and attempt to align the Effects: with the deferred function case. ]

Previous resolution [SUPERSEDED]:

This wording is relative to N4892.

  1. Modify 32.10.9 [futures.async] as indicated:

    template<class F, class... Args>
      [[nodiscard]] future<invoke_result_t<decay_t<F>, decay_t<Args>...>>
        async(F&& f, Args&&... args);
    template<class F, class... Args>
      [[nodiscard]] future<invoke_result_t<decay_t<F>, decay_t<Args>...>>
        async(launch policy, F&& f, Args&&... args);
    

    -2- Mandates: […]

    -3- Effects: The first function behaves the same as a call to the second function with a policy argument of launch::async | launch::deferred and the same arguments for F and Args. The second function creates a shared state that is associated with the returned future object. The further behavior of the second function depends on the policy argument as follows (if more than one of these conditions applies, the implementation may choose any of the corresponding policies):

    1. (3.1) — If launch::async is set in policy, calls invoke(decay-copy(std::forward<F>(f)), decay-copy(std::forward<Args>(args))...) (22.10.4 [func.require], 32.4.3.3 [thread.thread.constr]) as if in a new thread of execution represented by a thread object with the calls to decay-copy being evaluated in the thread that called async. Any return value is stored as the result in the shared state. Any exception propagated from the execution of invoke(decay-copy(std::forward<F>(f)), decay-copy(std::forward<Args>(args))...)call to invoke is stored as the exceptional result in the shared state. [Note ?: Exceptions from the decay-copy calls are propagated to the caller. — end note] The thread object is stored in the shared state and affects the behavior of any asynchronous return objects that reference that state.

    2. […]

Proposed resolution:

This wording is relative to N4892.

  1. Modify 32.10.9 [futures.async] as indicated:

    template<class F, class... Args>
      [[nodiscard]] future<invoke_result_t<decay_t<F>, decay_t<Args>...>>
        async(F&& f, Args&&... args);
    template<class F, class... Args>
      [[nodiscard]] future<invoke_result_t<decay_t<F>, decay_t<Args>...>>
        async(launch policy, F&& f, Args&&... args);
    

    -2- Mandates: […]

    -3- Effects: The first function behaves the same as a call to the second function with a policy argument of launch::async | launch::deferred and the same arguments for F and Args. The second function creates a shared state that is associated with the returned future object. The further behavior of the second function depends on the policy argument as follows (if more than one of these conditions applies, the implementation may choose any of the corresponding policies):

    1. (3.1) — If launch::async is set in policy, calls invoke(decay-copy(std::forward<F>(f)), decay-copy(std::forward<Args>(args))...) (22.10.4 [func.require], 32.4.3.3 [thread.thread.constr]) as if in a new thread of execution represented by a thread object with the calls to decay-copy being evaluated in the thread that called async. Any return value is stored as the result in the shared state. Any exception propagated from the execution of invoke(decay-copy(std::forward<F>(f)), decay-copy(std::forward<Args>(args)...)std::move(g), std::move(xyz)) is stored as the exceptional result in the shared state, where g is the result of decay-copy(std::forward<F>(f)) and xyz is the result of decay-copy(std::forward<Args>(args))... . [Note ?: Exceptions from the decay-copy calls are propagated to the caller. — end note] The thread object is stored in the shared state and affects the behavior of any asynchronous return objects that reference that state.

    2. […]

    […]

    -6- Throws: system_error if policy == launch::async and the implementation is unable to start a new thread; std::bad_alloc if memory for the internal data structures cannot be allocated; or any exception thrown by the initialization of the objects returned by the decay-copy calls.