4215. run_loop::finish should be noexcept

Section: 33.12.1 [exec.run.loop] Status: New Submitter: Eric Niebler Opened: 2025-02-13 Last modified: 2025-02-23

Priority: Not Prioritized

View all issues with New status.

Discussion:

Imported from cplusplus/sender-receiver #329.

run_loop::finish puts the run_loop into the finishing state so that the next time the work queue is empty, run_loop::run will return instead of waiting for more work.

Calling .finish() on a run_loop instance can potentially throw (finish() is not marked noexcept), that is because one valid implementation involves acquiring a lock on a std::mutex — a potentially throwing operation.

But failing to put the run_loop into the finishing state is problematic in the same way that a failing destructor is problematic: shutdown and clean-up code depends on it succeeding.

Consider sync_wait's use of run_loop:

sync-wait-state<Sndr> state;
auto op = connect(sndr, sync-wait-receiver<Sndr>{&state});
start(op);

state.loop.run();
if (state.error) {
  rethrow_exception(std::move(state.error));
}
return std::move(state.result);

It is the job of sync-wait-receiver to put the run_loop into the finishing state so that the invocation of state.loop.run() will return. It does that in its completion functions, like so:

void set_stopped() && noexcept;

Effects: Equivalent to state->loop.finish().

Here we are not handling the fact that state->loop.finish() is potentially throwing. Given that this function is noexcept, this will lead to the application getting terminated. Not good.

But even if we handle the exception and save it into state.result to be rethrown later, we still have a problem. Since run_loop::finish() threw, the run_loop has not been placed into the finishing state. That means that state.loop.run() will never return, and sync_wait will hang forever.

Simply put, run_loop::finish() has to be noexcept. The implementation must find a way to put the run_loop into the finishing state. If it cannot, it should terminate. Throwing an exception and foisting the problem on the caller — who has no recourse — is simply wrong.

Proposed resolution:

This wording is relative to N5001.

  1. Modify 33.12.1.1 [exec.run.loop.general] as indicated:

    namespace std::execution {
      class run_loop {
        // 33.12.1.2 [exec.run.loop.types], associated types
        class run-loop-scheduler; // exposition only
        class run-loop-sender;    // exposition only
        struct run-loop-opstate-base { // exposition only
          virtual void execute() = 0;  // exposition only
          run_loop* loop;              // exposition only
          run-loop-opstate-base* next; // exposition only
        };
        template<class Rcvr>
          using run-loop-opstate = unspecified; // exposition only
    
        // 33.12.1.4 [exec.run.loop.members], member functions
        run-loop-opstate-base* pop-front(); // exposition only
        void push-back(run-loop-opstate-base*); // exposition only
    
      public:
        // 33.12.1.3 [exec.run.loop.ctor], constructor and destructor
        run_loop() noexcept;
        run_loop(run_loop&&) = delete;
        ~run_loop();
    
        // 33.12.1.4 [exec.run.loop.members], member functions
        run-loop-scheduler get_scheduler();
        void run();
        void finish() noexcept;
      };
    }
    
  2. Modify 33.12.1.4 [exec.run.loop.members] as indicated:

    void finish() noexcept;
    

    -8- Preconditions: state is either starting or running.

    -9- Effects: Changes state to finishing.

    -10- Synchronization: finish synchronizes with the pop-front operation that returns nullptr.