Skip to content

Commit 6ac7829

Browse files
committed
exec::sequence: Exception Correctness
Corrected two exception-related problems with exec::sequence: - Previously exec::sequence senders unconditionally reported that they could throw when connected - Previously exec::sequence senders unconditionally reported that they could send set_error_t(std::exception_ptr), updated so this is now reported only if it's possible (i.e. if a child sender has that completion, or if any child sender other than the first possibly throws when connected)
1 parent 1a21827 commit 6ac7829

File tree

2 files changed

+107
-37
lines changed

2 files changed

+107
-37
lines changed

include/exec/sequence.hpp

Lines changed: 80 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
#include "../stdexec/__detail/__variant.hpp"
2020
#include "../stdexec/execution.hpp"
2121

22+
#include <type_traits>
23+
2224
STDEXEC_PRAGMA_PUSH()
2325
STDEXEC_PRAGMA_IGNORE_GNU("-Wmissing-braces")
2426

@@ -54,18 +56,11 @@ namespace experimental::execution
5456
STDEXEC_ATTRIBUTE(host, device)
5557
constexpr void _start_next() noexcept
5658
{
57-
STDEXEC_TRY
58-
{
59-
(*_start_next_)(this);
60-
}
61-
STDEXEC_CATCH_ALL
62-
{
63-
STDEXEC::set_error(static_cast<Rcvr&&>(_rcvr), std::current_exception());
64-
}
59+
(*_start_next_)(this);
6560
}
6661

6762
Rcvr _rcvr;
68-
void (*_start_next_)(_opstate_base*) = nullptr;
63+
void (*_start_next_)(_opstate_base*) noexcept = nullptr;
6964
};
7065

7166
template <class Rcvr>
@@ -120,8 +115,11 @@ namespace experimental::execution
120115
{
121116
template <class... _Ts>
122117
STDEXEC_ATTRIBUTE(host, device, always_inline)
123-
constexpr auto
124-
operator()(_Ts&&... __ts) const STDEXEC_AUTO_RETURN(_Tuple{static_cast<_Ts&&>(__ts)...});
118+
constexpr _Tuple
119+
operator()(_Ts&&... __ts) const noexcept(std::is_nothrow_constructible_v<_Tuple, _Ts...>)
120+
{
121+
return _Tuple{(_Ts&&) __ts...};
122+
}
125123
};
126124

127125
template <class Rcvr, class... Senders>
@@ -156,6 +154,12 @@ namespace experimental::execution
156154
template <class CvSndrs>
157155
STDEXEC_ATTRIBUTE(host, device)
158156
constexpr explicit _opstate(Rcvr&& rcvr, CvSndrs&& sndrs)
157+
noexcept(::STDEXEC::__nothrow_connectable<
158+
decltype(::STDEXEC::__get<0>(::STDEXEC::__declval<CvSndrs>())),
159+
_rcvr_t<sizeof...(Senders) == 0>>
160+
&& ::STDEXEC::__nothrow_invocable<decltype(::STDEXEC::__apply),
161+
__convert_tuple_fn<_senders_tuple_t>,
162+
CvSndrs>)
159163
: _opstate_base<Rcvr>{static_cast<Rcvr&&>(rcvr)}
160164
, _sndrs{STDEXEC::__apply(__convert_tuple_fn<_senders_tuple_t>{},
161165
static_cast<CvSndrs&&>(sndrs))}
@@ -170,19 +174,39 @@ namespace experimental::execution
170174
}
171175

172176
template <std::size_t Remaining>
173-
static constexpr void _start_next(_opstate_base<Rcvr>* _self)
177+
static constexpr void _start_next(_opstate_base<Rcvr>* _self) noexcept
174178
{
175-
constexpr auto __nth = sizeof...(Senders) - Remaining;
176-
auto* self = static_cast<_opstate*>(_self);
177-
auto& sndr = STDEXEC::__get<__nth + 1>(self->_sndrs);
178-
auto& op = self->_ops.template __emplace_from<__nth + 1>(STDEXEC::connect,
179-
std::move(sndr),
180-
_rcvr_t<Remaining == 1>{self});
181-
if constexpr (Remaining > 1)
179+
constexpr auto __nth = sizeof...(Senders) - Remaining;
180+
auto* self = static_cast<_opstate*>(_self);
181+
auto& sndr = STDEXEC::__get<__nth + 1>(self->_sndrs);
182+
constexpr auto nothrow = noexcept(
183+
self->_ops.template __emplace_from<__nth + 1>(STDEXEC::connect,
184+
std::move(sndr),
185+
_rcvr_t < Remaining == 1 > {self}));
186+
STDEXEC_TRY
187+
{
188+
auto& op = self->_ops.template __emplace_from<__nth + 1>(STDEXEC::connect,
189+
std::move(sndr),
190+
_rcvr_t < Remaining
191+
== 1 > {self});
192+
if constexpr (Remaining > 1)
193+
{
194+
self->_start_next_ = &_start_next<Remaining - 1>;
195+
}
196+
STDEXEC::start(op);
197+
}
198+
STDEXEC_CATCH_ALL
182199
{
183-
self->_start_next_ = &_start_next<Remaining - 1>;
200+
if constexpr (nothrow)
201+
{
202+
STDEXEC_UNREACHABLE();
203+
}
204+
else
205+
{
206+
STDEXEC::set_error(static_cast<Rcvr&&>(static_cast<_opstate*>(_self)->_rcvr),
207+
std::current_exception());
208+
}
184209
}
185-
STDEXEC::start(op);
186210
}
187211

188212
STDEXEC_ATTRIBUTE(host, device)
@@ -202,36 +226,48 @@ namespace experimental::execution
202226
// The completions of the sequence sender are the error and stopped completions of all the
203227
// child senders plus the value completions of the last child sender.
204228
template <class... Env>
205-
struct _completions_fn
229+
struct _completions_fn;
230+
231+
template <class Env>
232+
struct _completions_fn<Env>
206233
{
207-
// When folding left, the first sender folded will be the last sender in the list. That is
208-
// also when the "state" of the fold is void. For this case we want to include the value
209-
// completions; otherwise, we want to exclude them.
210-
template <class State, class... Args>
234+
template <bool First, bool AddExceptionPtr, class Sender>
235+
static constexpr bool _add_exception_ptr =
236+
AddExceptionPtr
237+
|| !(First || STDEXEC::__nothrow_connectable<Sender, STDEXEC::__receiver_archetype<Env>>);
238+
239+
// When folding left, the first sender folded will be the last sender in the list.
240+
// For this case we want to include the value completions; otherwise, we want to
241+
// exclude them.
242+
template <bool First, bool AddExceptionPtr, class... Args>
211243
struct _fold_left;
212244

213-
template <class State, class Head, class... Tail>
214-
struct _fold_left<State, Head, Tail...>
245+
template <bool First, bool AddExceptionPtr, class Head, class... Tail>
246+
struct _fold_left<First, AddExceptionPtr, Head, Tail...>
215247
{
216248
using __t = STDEXEC::__gather_completion_signatures_t<
217-
STDEXEC::__completion_signatures_of_t<Head, Env...>,
249+
STDEXEC::__completion_signatures_of_t<Head, Env>,
218250
STDEXEC::set_value_t,
219251
STDEXEC::__mconst<STDEXEC::completion_signatures<>>::__f,
220252
STDEXEC::__cmplsigs::__default_completion,
221253
STDEXEC::__mtry_q<STDEXEC::__concat_completion_signatures_t>::__f,
222-
STDEXEC::__t<_fold_left<State, Tail...>>>;
254+
STDEXEC::__t<
255+
_fold_left<false, _add_exception_ptr<First, AddExceptionPtr, Head>, Tail...>>>;
223256
};
224257

225-
template <class Head>
226-
struct _fold_left<void, Head>
258+
template <bool First, bool AddExceptionPtr, class Head>
259+
struct _fold_left<First, AddExceptionPtr, Head>
227260
{
228261
using __t = STDEXEC::__mtry_q<STDEXEC::__concat_completion_signatures_t>::__f<
229-
STDEXEC::completion_signatures<STDEXEC::set_error_t(std::exception_ptr)>,
230-
STDEXEC::__completion_signatures_of_t<Head, Env...>>;
262+
std::conditional_t<
263+
_add_exception_ptr<First, AddExceptionPtr, Head>,
264+
STDEXEC::completion_signatures<STDEXEC::set_error_t(std::exception_ptr)>,
265+
STDEXEC::completion_signatures<>>,
266+
STDEXEC::__completion_signatures_of_t<Head, Env>>;
231267
};
232268

233269
template <class... Sender>
234-
using __f = STDEXEC::__t<_fold_left<void, Sender...>>;
270+
using __f = STDEXEC::__t<_fold_left<true, false, Sender...>>;
235271
};
236272

237273
template <class Sender0, class... Senders>
@@ -248,7 +284,11 @@ namespace experimental::execution
248284
STDEXEC_ATTRIBUTE(host, device)
249285
static consteval auto get_completion_signatures()
250286
{
251-
if constexpr (STDEXEC::__decay_copyable<Self>)
287+
if constexpr (sizeof...(Env) == 0)
288+
{
289+
return STDEXEC::__dependent_sender<Self>();
290+
}
291+
else if constexpr (STDEXEC::__decay_copyable<Self>)
252292
{
253293
return _completions_t<Self, Env...>{};
254294
}
@@ -263,6 +303,10 @@ namespace experimental::execution
263303
template <STDEXEC::__decay_copyable Self, class Rcvr>
264304
STDEXEC_ATTRIBUTE(host, device)
265305
constexpr STDEXEC_EXPLICIT_THIS_BEGIN(auto connect)(this Self&& self, Rcvr rcvr)
306+
noexcept(std::is_nothrow_constructible_v<
307+
_opstate<Rcvr, STDEXEC::__copy_cvref_t<Self, Sender0>, Senders...>,
308+
Rcvr,
309+
decltype(((Self&&) self)._sndrs)>)
266310
{
267311
return _opstate<Rcvr, STDEXEC::__copy_cvref_t<Self, Sender0>, Senders...>{
268312
static_cast<Rcvr&&>(rcvr),

test/exec/test_sequence.cpp

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ TEST_CASE("sequence produces a sender", "[sequence]") {
6666
STATIC_REQUIRE(!ex::__callable<exec::sequence_t>);
6767

6868
auto s0 = exec::sequence(ex::just(42));
69+
static_assert(ex::__nothrow_connectable<decltype(s0), ex::__receiver_archetype<ex::env<>>>);
6970
STATIC_REQUIRE(ex::sender<decltype(s0)>);
7071
STATIC_REQUIRE(ex::sender_in<decltype(s0)>);
7172
check_val_types<ex::__mset<pack<int>>>(s0);
@@ -80,8 +81,24 @@ TEST_CASE("sequence produces a sender", "[sequence]") {
8081
STATIC_REQUIRE(!ex::sender_in<decltype(s1)>);
8182
STATIC_REQUIRE(ex::sender_in<decltype(s1), env_t>);
8283
check_val_types<ex::__mset<pack<const std::allocator<void>&>>, env_t>(s1);
83-
check_err_types<ex::__mset<std::exception_ptr, int>, env_t>(s1);
84+
check_err_types<ex::__mset<int>, env_t>(s1);
8485
check_sends_stopped<false, env_t>(s1);
86+
87+
auto s2 = exec::sequence(ex::just(), ex::just(42));
88+
STATIC_REQUIRE(ex::sender<decltype(s2)>);
89+
STATIC_REQUIRE(!ex::sender_in<decltype(s2)>);
90+
STATIC_REQUIRE(ex::sender_in<decltype(s2), ex::env<>>);
91+
check_val_types<ex::__mset<pack<int>>>(s2);
92+
check_err_types<ex::__mset<>>(s2);
93+
check_sends_stopped<false>(s2);
94+
95+
auto s3 = exec::sequence(ex::just(true), ex::just(42));
96+
STATIC_REQUIRE(ex::sender<decltype(s3)>);
97+
STATIC_REQUIRE(!ex::sender_in<decltype(s3)>);
98+
STATIC_REQUIRE(ex::sender_in<decltype(s3), ex::env<>>);
99+
check_val_types<ex::__mset<pack<int>>>(s3);
100+
check_err_types<ex::__mset<>>(s3);
101+
check_sends_stopped<false>(s3);
85102
}
86103

87104
TEST_CASE("sequence with one argument works", "[sequence]") {
@@ -136,7 +153,16 @@ TEST_CASE("sequence with two arguments works", "[sequence]") {
136153
TEST_CASE("sequence with sender with throwing connect", "[sequence]") {
137154
auto err = std::make_exception_ptr(connect_exception{});
138155
auto sndr = exec::sequence(ex::just(big{}), throwing_connect{}, ex::just(big{}, 42));
156+
check_err_types<ex::__mset<std::exception_ptr>, ex::env<>>(std::move(sndr));
139157
auto op = ex::connect(std::move(sndr), expect_error_receiver{err});
140158
ex::start(op);
141159
}
160+
161+
TEST_CASE("sequence with sender with only first sender with throwing connect", "[sequence]")
162+
{
163+
auto err = std::make_exception_ptr(connect_exception{});
164+
auto sndr = exec::sequence(throwing_connect{}, ex::just());
165+
check_err_types<ex::__mset<>, ex::env<>>(sndr);
166+
CHECK_THROWS(ex::connect(std::move(sndr), empty_recv::recv0{}));
167+
}
142168
#endif // !STDEXEC_NO_STD_EXCEPTIONS()

0 commit comments

Comments
 (0)