diff --git a/include/stdexec/__detail/__stop_when.hpp b/include/stdexec/__detail/__stop_when.hpp index abec35ec4..5c36758ae 100644 --- a/include/stdexec/__detail/__stop_when.hpp +++ b/include/stdexec/__detail/__stop_when.hpp @@ -120,10 +120,14 @@ namespace STDEXEC using __cb1_t = _Token1::template callback_type<__cb_t>; using __cb2_t = _Token2::template callback_type<__cb_t>; + // __called_ must be initialized before the callbacks: a callback + // registered with an already-stopped token runs its function as part + // of the callback's own construction, and that function reads and + // writes __called_. _Fn __fn_; + __std::atomic __called_{false}; __cb1_t __cb1_; __cb2_t __cb2_; - __std::atomic __called_{false}; }; _Token1 __tkn1_; diff --git a/test/stdexec/algos/adaptors/test_stop_when.cpp b/test/stdexec/algos/adaptors/test_stop_when.cpp index 4b76c8c84..feb4d1365 100644 --- a/test/stdexec/algos/adaptors/test_stop_when.cpp +++ b/test/stdexec/algos/adaptors/test_stop_when.cpp @@ -70,6 +70,43 @@ namespace wait_for_value(ex::__stop_when(isTokenStoppable(), source.get_token()), true); } +#if !STDEXEC_USE_MODULES() + // TODO: figure out why this fails in a modules build + TEST_CASE("callbacks registered with an already-stopped fused token run exactly once", + "[adaptors][stop-when]") + { + // Regression test for #2326: the first token is already stopped when the + // callback is registered, so the function must run once, at registration, + // and never again when the second token is stopped later. + ex::inplace_stop_source first, second; + first.request_stop(); + + int calls = 0; + int atReg = -1; + auto makeCallback = [](auto token, auto fn) noexcept + { + using cb_t = decltype(token)::template callback_type; + return std::optional(std::in_place, std::move(token), std::move(fn)); + }; + + ex::sync_wait(ex::__stop_when(ex::__stop_when(ex::read_env(ex::get_stop_token) + | ex::then( + [&](auto token) noexcept + { + auto cb = makeCallback(token, + [&]() noexcept + { ++calls; }); + atReg = calls; + second.request_stop(); + }), + first.get_token()), + second.get_token())); + + REQUIRE(atReg == 1); + REQUIRE(calls == 1); + } +#endif + #if !STDEXEC_USE_MODULES() // TODO: figure out why this fails in a modules build TEST_CASE("stop-when fuses its token with the receiver's when both are stoppable",