From 183060e092d197fe7b30b53c09f9e675bbb62d35 Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Thu, 8 Oct 2026 13:39:56 +0200 Subject: [PATCH 1/4] Fix transform_each subscription exception handling --- include/exec/sequence/transform_each.hpp | 3 +- test/exec/sequence/test_transform_each.cpp | 83 ++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/include/exec/sequence/transform_each.hpp b/include/exec/sequence/transform_each.hpp index ca1ca923e..9267636de 100644 --- a/include/exec/sequence/transform_each.hpp +++ b/include/exec/sequence/transform_each.hpp @@ -125,7 +125,8 @@ namespace experimental::execution template auto operator()(__ignore, _Adaptor __adaptor, _Sequence&& __sequence) - noexcept(__nothrow_decay_copyable<_Adaptor, _Sequence>) + noexcept(__nothrow_decay_copyable<_Adaptor, _Sequence> + && __nothrow_subscribable<_Sequence, __receiver<_Receiver, _Adaptor>>) -> __operation<_Sequence, _Receiver, _Adaptor> { return {static_cast<_Sequence&&>(__sequence), diff --git a/test/exec/sequence/test_transform_each.cpp b/test/exec/sequence/test_transform_each.cpp index 919aba25a..ca14f1cc2 100644 --- a/test/exec/sequence/test_transform_each.cpp +++ b/test/exec/sequence/test_transform_each.cpp @@ -28,6 +28,10 @@ #include #include +#include +#include +#include + namespace { @@ -43,6 +47,85 @@ namespace void set_value() noexcept {} }; + TEST_CASE("transform_each - subscribe is noexcept for a nonthrowing child", + "[sequence_senders][transform_each]") + { + auto transformed = exec::transform_each(exec::empty_sequence(), ex::then([]() noexcept {})); + + STATIC_REQUIRE(noexcept(exec::subscribe(transformed, next_rcvr{}))); + STATIC_REQUIRE(noexcept(exec::subscribe(std::as_const(transformed), next_rcvr{}))); + STATIC_REQUIRE(noexcept(exec::subscribe(std::move(transformed), next_rcvr{}))); + + auto op = exec::subscribe(std::move(transformed), next_rcvr{}); + ex::start(op); + } + +#if !STDEXEC_NO_STDCPP_EXCEPTIONS() + struct range_error + {}; + + template + struct throwing_range + { + using iterator = std::array::iterator; + + auto begin() noexcept(!ThrowFromBegin) -> iterator + { + if constexpr (ThrowFromBegin) + { + throw range_error{}; + } + return values_.begin(); + } + + auto end() noexcept(ThrowFromBegin) -> iterator + { + if constexpr (!ThrowFromBegin) + { + throw range_error{}; + } + return values_.end(); + } + + std::array values_{0}; + }; + + struct range_rcvr : next_rcvr + { + void set_error(std::exception_ptr) noexcept {} + + void set_stopped() noexcept {} + }; + + TEMPLATE_TEST_CASE("transform_each - subscribe propagates range exceptions", + "[sequence_senders][transform_each][iterate]", + throwing_range, + throwing_range) + { + TestType range; + auto transformed = exec::transform_each(exec::iterate(std::views::all(range)), + ex::then([](int) noexcept {})); + + SECTION("lvalue sender") + { + STATIC_REQUIRE_FALSE(noexcept(exec::subscribe(transformed, range_rcvr{}))); + CHECK_THROWS_AS(exec::subscribe(transformed, range_rcvr{}), range_error); + } + + SECTION("const lvalue sender") + { + STATIC_REQUIRE_FALSE(noexcept(exec::subscribe(std::as_const(transformed), range_rcvr{}))); + CHECK_THROWS_AS(exec::subscribe(std::as_const(transformed), range_rcvr{}), range_error); + } + + SECTION("rvalue sender") + { + STATIC_REQUIRE_FALSE(noexcept(exec::subscribe(std::move(transformed), range_rcvr{}))); + CHECK_THROWS_AS(exec::subscribe(std::move(transformed), range_rcvr{}), range_error); + } + } +#endif // !STDEXEC_NO_STDCPP_EXCEPTIONS() + TEST_CASE("transform_each - transform sender applies adaptor to no elements", "[sequence_senders][transform_each][empty_sequence]") { From 6f42db3a369bed714a52f1c52f7a51e22fe96b89 Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Thu, 8 Oct 2026 13:53:17 +0200 Subject: [PATCH 2/4] Include ranges for transform_each throwing-range tests --- test/exec/sequence/test_transform_each.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/test/exec/sequence/test_transform_each.cpp b/test/exec/sequence/test_transform_each.cpp index ca14f1cc2..dd920941c 100644 --- a/test/exec/sequence/test_transform_each.cpp +++ b/test/exec/sequence/test_transform_each.cpp @@ -30,6 +30,7 @@ #include #include +#include #include namespace From 0c42f102b8170e39d855f5428044e8ea45ae148c Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Thu, 8 Oct 2026 14:09:21 +0200 Subject: [PATCH 3/4] Fix transform_each noexcept for throwing receiver moves --- include/exec/sequence/transform_each.hpp | 1 + test/exec/sequence/test_transform_each.cpp | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/include/exec/sequence/transform_each.hpp b/include/exec/sequence/transform_each.hpp index 9267636de..9d325deed 100644 --- a/include/exec/sequence/transform_each.hpp +++ b/include/exec/sequence/transform_each.hpp @@ -126,6 +126,7 @@ namespace experimental::execution template auto operator()(__ignore, _Adaptor __adaptor, _Sequence&& __sequence) noexcept(__nothrow_decay_copyable<_Adaptor, _Sequence> + && __nothrow_move_constructible<_Receiver> && __nothrow_subscribable<_Sequence, __receiver<_Receiver, _Adaptor>>) -> __operation<_Sequence, _Receiver, _Adaptor> { diff --git a/test/exec/sequence/test_transform_each.cpp b/test/exec/sequence/test_transform_each.cpp index dd920941c..dbfeb265b 100644 --- a/test/exec/sequence/test_transform_each.cpp +++ b/test/exec/sequence/test_transform_each.cpp @@ -65,6 +65,25 @@ namespace struct range_error {}; + struct throwing_move_rcvr : next_rcvr + { + throwing_move_rcvr() = default; + + throwing_move_rcvr(throwing_move_rcvr&&) + { + throw range_error{}; + } + }; + + TEST_CASE("transform_each - subscribe propagates receiver move exceptions", + "[sequence_senders][transform_each]") + { + auto transformed = exec::transform_each(exec::empty_sequence(), ex::then([]() noexcept {})); + + STATIC_REQUIRE_FALSE(noexcept(exec::subscribe(transformed, throwing_move_rcvr{}))); + CHECK_THROWS_AS(exec::subscribe(transformed, throwing_move_rcvr{}), range_error); + } + template struct throwing_range { From 88237003e04de0c1a1ae70fdee7ea6ce253a32bc Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Thu, 8 Oct 2026 14:28:02 +0200 Subject: [PATCH 4/4] Cover throwing adaptor copies in transform_each subscription --- test/exec/sequence/test_transform_each.cpp | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/exec/sequence/test_transform_each.cpp b/test/exec/sequence/test_transform_each.cpp index dbfeb265b..841f488b6 100644 --- a/test/exec/sequence/test_transform_each.cpp +++ b/test/exec/sequence/test_transform_each.cpp @@ -84,6 +84,29 @@ namespace CHECK_THROWS_AS(exec::subscribe(transformed, throwing_move_rcvr{}), range_error); } + struct throwing_copy_value + { + throwing_copy_value() = default; + + throwing_copy_value(throwing_copy_value const &) + { + throw range_error{}; + } + + throwing_copy_value(throwing_copy_value&&) noexcept = default; + }; + + TEST_CASE("transform_each - subscribe propagates adaptor copy exceptions", + "[sequence_senders][transform_each]") + { + auto transformed = exec::transform_each( + exec::empty_sequence(), + ex::then([capture = throwing_copy_value{}]() noexcept { (void) capture; })); + + STATIC_REQUIRE_FALSE(noexcept(exec::subscribe(transformed, next_rcvr{}))); + CHECK_THROWS_AS(exec::subscribe(transformed, next_rcvr{}), range_error); + } + template struct throwing_range {