This page is a snapshot from the LWG issues list, see the Library Active Issues List for more information and the meaning of Ready status.

4198. schedule_from isn't starting the schedule sender if decay-copying results throws

Section: 33.9.12.5 [exec.schedule.from] Status: Ready Submitter: Eric Niebler Opened: 2025-02-03 Last modified: 2025-02-13

Priority: 1

View other active issues in [exec.schedule.from].

View all other issues in [exec.schedule.from].

View all issues with Ready status.

Discussion:

Imported from cplusplus/sender-receiver #304.

33.9.12.5 [exec.schedule.from]/p11 specifies schedule_from's completion operation as follows:


[]<class Tag, class... Args>(auto, auto& state, auto& rcvr, Tag, Args&&... args) noexcept
    -> void {
  using result_t = decayed-tuple<Tag, Args...>;
  constexpr bool nothrow = is_nothrow_constructible_v<result_t, Tag, Args...>;

  try {
    state.async-result.template emplace<result_t>(Tag(), std::forward<Args>(args)...);
  } catch (...) {
    if constexpr (!nothrow) {
      set_error(std::move(rcvr), current_exception());
      return;
    }
  }
  start(state.op-state);
};
so if emplacing the result tuple throws, set_error is immediately called on the downstream receiver. no attempt is made to post the completion to the specified scheduler. this is probably not the right behavior.

Suggested resolution

The right thing, i think, is to catch the exception, emplace the exception_ptr into the async-result variant, and then start the schedule operation, as shown below:


[]<class Tag, class... Args>(auto, auto& state, auto& rcvr, Tag, Args&&... args) noexcept
    -> void {
  using result_t = decayed-tuple<Tag, Args...>;
  constexpr bool nothrow = is_nothrow_constructible_v<result_t, Tag, Args...>;

  try {
    state.async-result.template emplace<result_t>(Tag(), std::forward<Args>(args)...);
  } catch(...) {
    if constexpr (!nothrow)
      state.async-result.template emplace<tuple<set_error_t, exception_ptr>>(set_error, current_exception());
  }

  start(state.op-state);
}
we also need to change how we specify the variant type of state.async-result:
Let Sigs be a pack of the arguments to the completion_signatures specialization named by completion_signatures_of_t<child-type<Sndr>, env_of_t<Rcvr>>. Let as-tuple be an alias template that transforms a completion signature Tag(Args...) into the tuple specialization decayed-tuple<Tag, Args...>. such that as-tuple<Tag(Args...)> denotes the tuple specialization decayed-tuple<Tag, Args...>, and let is-nothrow-decay-copy-sig be a variable template such that is-nothrow-decay-copy-sig<Tag(Args...)> is a core constant expression of type bool const and whose value is true if the types Args... are all nothrow decay-copyable, and false otherwise. Let error-completion be a pack consisting of the type set_error_t(exception_ptr) if (is-nothrow-decay-copy-sig<Sigs> &&...) is false, and an empty pack otherwise. Then variant_t denotes the type variant<monostate, as-tuple<Sigs>..., error-completion...>, except with duplicate types removed.

[This touches the same text as LWG 4203(i).]

[2025-02-07; Reflector poll]

Set priority to 1 after reflector poll.

[Hagenberg 2025-02-11; LWG]

Direction seems right. Decay-copyable is not a defined term.

[2025-02-12 Tim adds PR]

This also corrects the issue that nothrow is currently relying on the unspecified exception specification of tuple's constructor.

[Hagenberg 2025-02-12; move to Ready]

Proposed resolution:

This wording is relative to N5001.

  1. Modify 33.9.12.5 [exec.schedule.from] p8 as indicated:

    -8- Let Sigs be a pack of the arguments to the completion_signatures specialization named by completion_signatures_of_t<child-type<Sndr>, env_of_t<Rcvr>>. Let as-tuple be an alias template that transforms a completion signature Tag(Args...) into the tuple specialization decayed-tuple<Tag, Args...>. such that as-tuple<Tag(Args...)> denotes the type decayed-tuple<Tag, Args...>, and let is-nothrow-decay-copy-sig be a variable template such that auto(is-nothrow-decay-copy-sig<Tag(Args...)>) is a constant expression of type bool and equal to (is_nothrow_constructible_v<decay_t<Args>, Args> && ...). Let error-completion be a pack consisting of the type set_error_t(exception_ptr) if (is-nothrow-decay-copy-sig<Sigs> &&...) is false, and an empty pack otherwise. Then variant_t denotes the type variant<monostate, as-tuple<Sigs>..., error-completion...>, except with duplicate types removed.

  2. Modify 33.9.12.5 [exec.schedule.from] p11 as indicated:

    -11- The member impls-for<schedule_from_t>::complete is initialized with a callable object equivalent to the following lambda:

    
    []<class Tag, class... Args>(auto, auto& state, auto& rcvr, Tag, Args&&... args) noexcept
        -> void {
      using result_t = decayed-tuple<Tag, Args...>;
      constexpr bool nothrow = is_nothrow_constructible_v<result_t, Tag, Args...>
                               (is_nothrow_constructible_v<decay_t<Args>, Args> && ...);
    
      try {
        state.async-result.template emplace<result_t>(Tag(), std::forward<Args>(args)...);
      } catch (...) {
        if constexpr (!nothrow) {
          state.async-result.template emplace<tuple<set_error_t, exception_ptr>>(set_error, current_exception());
          set_error(std::move(rcvr), current_exception());
          return;
        }
      }
      start(state.op-state);
    };