Title
Uses of MANDATE-NOTHROW in CPOs should not enclose CPO argument sub-expressions
Status
new
Section
[exec]
Submitter
Lewis Baker

Created on 2025-08-25.00:00:00 last changed 3 weeks ago

Messages

Date: 2025-08-25.00:00:00

There are a number of CPOs defined in [exec] which have behaviour specified in terms of being expression-equivalent to a MANDATE-NOTHROW expression.

The intent of this is that we want to make sure that the call that the CPO dispatches to is marked `noexcept`.

However, the way that these CPOs are currently specified in terms of sub-expressions means that we are currently requiring that all of the expressions passed as arguments to the CPO are also `noexcept`. Outside of defining these CPOs as preprocessor macros, this is unimplementable — and also undesirable behaviour.

For example, [exec.set.value] defines `set_value(rcvr, vs...)` to be equivalent to MANDATE-NOTHROW(rcvr.set_value(vs...)) for sub-expressions `rcvr` and pack of sub-expressions `vs`.

In [exec.general] p5 we define MANDATE-NOTHROW(expr) as expression-equivalent to `expr` but mandate that `noexcept(expr)` is `true`.

So in the above definition of `set_value(rcvr, vs...)` we are actually requiring that the expression `noexcept(rcvr.set_value(vs...))` is `true`.

This is only true if all of the sub-expressions are `noexcept`, i.e. all of the following expressions are `true`.

  • noexcept(rcvr),

  • (noexcept(vs) && ...),

  • the member-function call to `rcvr.set_value(vs...)` including any implicit conversions of arguments.

This means that if, for example, one of the sub-expressions in the pack `vs` was a call to some potentially-throwing function then the overall `set_value` expression would be violating the mandates requirement.

For example:

struct my_receiver 
{
  void set_value(int x) noexcept;
};

int get_value() noexcept(false);

my_receiver r;
std::execution::set_value(r, get_value()); // fails MANDATE-NOTHROW mandates

Instead, we need to redefine these CPOs as being expression-equivalent to something that does not require that the argument expressions to the CPO themselves are `noexcept` — only what will be in the body of the CPO function.

For example, we could change [exec.set.value] to define `set_value(rcvr, vs...)` as expression-equivalent to:

[](auto&& rcvr2, auto&&... vs2) noexcept -> 
  decltype(auto) requires requires { std::forward<decltype(rcvr2)>(rcvr2).set_value(std::forward<decltype(vs2)>(vs2)...); } 
{
  return MANDATE-NOTHROW(std::forward<decltype(rcvr2)>(rcvr2).set_value(std::forward<decltype(vs2)>(vs2)...));
}(rcvr, vs...)

The following sections all contain problematic uses of MANDATE-NOTHROW:

  • [exec.get.allocator]

  • [exec.get.stop.token]

  • [exec.get.env]

  • [exec.get.domain]

  • [exec.get.scheduler]

  • [exec.get.delegation.scheduler]

  • [exec.get.fwd.progress]

  • [exec.get.compl.sched]

  • [exec.get.await.adapt]

  • [exec.set.value]

  • [exec.set.error]

  • [exec.set.stopped]

  • [exec.opstate.start]

History
Date User Action Args
2025-08-25 00:00:00admincreate