Skip to content

Commit 957991f

Browse files
committed
task: co_return with_error(...) & with_stopped
Implemented P4282 by enhancing task as follows: - co_yield can now be used directly to send set_stopped using the new with_stopped tag - co_return can now be used with with_error and with_stopped to send set_error or set_stopped, respectively Note that the co_return functionality requires a compiler which implements P3950 (adopted into the C++29 working draft in Brno) when used with a task<void>.
1 parent b5747c0 commit 957991f

3 files changed

Lines changed: 232 additions & 21 deletions

File tree

include/stdexec/__detail/__config.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,12 @@ STDEXEC_NAMESPACE_STD_END
252252
// clang-format on
253253

254254
////////////////////////////////////////////////////////////////////////////////////////////////////
255+
#if defined(__cpp_impl_coroutine) && __cpp_impl_coroutine >= 202606L
256+
# define STDEXEC_NO_STDCPP_COROUTINE_RETURN_VOID_AND_VALUE() 0
257+
#else
258+
# define STDEXEC_NO_STDCPP_COROUTINE_RETURN_VOID_AND_VALUE() 1
259+
#endif
260+
255261
#if __cpp_impl_coroutine >= 201902L && __cpp_lib_coroutine >= 201902L
256262
# if !STDEXEC_USE_MODULES()
257263
// we've already imported std above

include/stdexec/__detail/__task.hpp

Lines changed: 76 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,28 @@ STDEXEC_PRAGMA_IGNORE_GNU("-Wmismatched-new-delete")
4646
namespace STDEXEC
4747
{
4848
# if !STDEXEC_NO_STDCPP_COROUTINES()
49+
////////////////////////////////////////////////////////////////////////////////
50+
// STDEXEC::with_error
51+
template <class _Error>
52+
struct with_error
53+
{
54+
using type = __decay_t<_Error>;
55+
type error;
56+
};
57+
58+
template <class _Error>
59+
STDEXEC_HOST_DEVICE_DEDUCTION_GUIDE with_error(_Error) -> with_error<_Error>;
60+
61+
////////////////////////////////////////////////////////////////////////////////
62+
// STDEXEC::with_stopped
63+
struct with_stopped
64+
{};
65+
4966
namespace __task
5067
{
5168
////////////////////////////////////////////////////////////////////////////////
5269
// A base class for task::promise_type so it can be specialized when _Ty is void:
53-
template <class _Ty>
70+
template <class _Promise, class _Ty>
5471
struct __promise_base
5572
{
5673
template <class _Value = _Ty>
@@ -59,6 +76,20 @@ namespace STDEXEC
5976
__result_.emplace(static_cast<_Value&&>(__value));
6077
}
6178

79+
template <class _Error>
80+
requires (!__std::convertible_to<with_error<_Error>, _Ty>)
81+
constexpr void return_value(with_error<_Error> __error) //
82+
noexcept(noexcept(static_cast<_Promise&>(*this).__set_error(std::move(__error).error)))
83+
{
84+
static_cast<_Promise&>(*this).__set_error(std::move(__error).error);
85+
}
86+
87+
constexpr void return_value(with_stopped) noexcept
88+
requires (!__std::convertible_to<with_stopped, _Ty>)
89+
{
90+
static_cast<_Promise&>(*this).__set_stopped();
91+
}
92+
6293
[[nodiscard]]
6394
constexpr auto __result() noexcept -> _Ty&
6495
{
@@ -68,10 +99,25 @@ namespace STDEXEC
6899
__optional<_Ty> __result_{};
69100
};
70101

71-
template <>
72-
struct __promise_base<void>
102+
template <class _Promise>
103+
struct __promise_base<_Promise, void>
73104
{
74105
constexpr void return_void() {}
106+
107+
# if !STDEXEC_NO_STDCPP_COROUTINE_RETURN_VOID_AND_VALUE()
108+
template <class _Error>
109+
constexpr void return_value(with_error<_Error> __error) //
110+
noexcept(noexcept(static_cast<_Promise&>(*this).__set_error(std::move(__error).error)))
111+
{
112+
static_cast<_Promise&>(*this).__set_error(std::move(__error).error);
113+
}
114+
115+
constexpr void return_value(with_stopped) noexcept
116+
{
117+
static_cast<_Promise&>(*this).__set_stopped();
118+
}
119+
# endif
120+
75121
constexpr void __result() {}
76122
};
77123

@@ -261,18 +307,6 @@ namespace STDEXEC
261307
} __throw_error{};
262308
} // namespace __task
263309

264-
////////////////////////////////////////////////////////////////////////////////
265-
// STDEXEC::with_error
266-
template <class _Error>
267-
struct with_error
268-
{
269-
using type = __decay_t<_Error>;
270-
type error;
271-
};
272-
273-
template <class _Error>
274-
STDEXEC_HOST_DEVICE_DEDUCTION_GUIDE with_error(_Error) -> with_error<_Error>;
275-
276310
////////////////////////////////////////////////////////////////////////////////
277311
// STDEXEC::task
278312
template <class _Ty = void, class _TaskEnv = env<>>
@@ -471,6 +505,7 @@ namespace STDEXEC
471505
_TaskEnv __env_;
472506
task __task_;
473507
__error_variant_t __errors_{__no_init};
508+
bool __stopped_{};
474509
};
475510

476511
template <class _Env>
@@ -527,6 +562,10 @@ namespace STDEXEC
527562
[[nodiscard]]
528563
auto __completed() noexcept -> __std::coroutine_handle<> final
529564
{
565+
if (this->__stopped_)
566+
{
567+
return STDEXEC::__coroutine_unhandled_stopped(this->__handle());
568+
}
530569
this->__reset_callback();
531570
return this->__handle().promise().continuation().handle();
532571
}
@@ -581,7 +620,7 @@ namespace STDEXEC
581620
// task<T,E>::promise_type
582621
template <class _Ty, class _TaskEnv>
583622
struct STDEXEC_ATTRIBUTE(empty_bases) task<_Ty, _TaskEnv>::__promise
584-
: __task::__promise_base<_Ty>
623+
: __task::__promise_base<__promise, _Ty>
585624
, with_awaitable_senders<__promise>
586625
{
587626
__promise() noexcept = default;
@@ -641,11 +680,9 @@ namespace STDEXEC
641680
}
642681

643682
template <class _Error>
644-
constexpr auto yield_value(with_error<_Error> __error) //
645-
noexcept(__nothrow_error_conversion<typename with_error<_Error>::type&&>())
683+
constexpr void __set_error(_Error&& __error) noexcept(__nothrow_error_conversion<_Error&&>())
646684
{
647-
using __source_t = typename with_error<_Error>::type&&;
648-
using __is_convertible_error = __mbind_front_q<__mconvertible_to, __source_t>;
685+
using __is_convertible_error = __mbind_front_q<__mconvertible_to, _Error&&>;
649686
constexpr auto __count =
650687
__mapply<__mcount_if<__is_convertible_error>, __error_variant_t>::value;
651688
static_assert(__count == 1,
@@ -654,8 +691,26 @@ namespace STDEXEC
654691
{
655692
using __error_t =
656693
__mapply<__mfind_if<__is_convertible_error, __q<__mfront>>, __error_variant_t>;
657-
__state_->__errors_.template emplace<__error_t>(std::move(__error).error);
694+
__state_->__errors_.template emplace<__error_t>(static_cast<_Error&&>(__error));
658695
}
696+
}
697+
698+
template <class _Error>
699+
constexpr auto yield_value(with_error<_Error> __error) //
700+
noexcept(noexcept(__set_error(std::move(__error).error)))
701+
{
702+
__set_error(std::move(__error).error);
703+
return __completed_awaiter{};
704+
}
705+
706+
constexpr void __set_stopped() noexcept
707+
{
708+
__state_->__stopped_ = true;
709+
}
710+
711+
constexpr auto yield_value(with_stopped) noexcept
712+
{
713+
__set_stopped();
659714
return __completed_awaiter{};
660715
}
661716

test/stdexec/types/test_task.cpp

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,82 @@ namespace
208208
CHECK(destroyed);
209209
}
210210

211+
auto test_task_yields_stopped([[maybe_unused]] destruction_probe probe) -> ex::task<void>
212+
{
213+
co_yield ex::with_stopped();
214+
FAIL("Expected co_yielding with_stopped to stop the task");
215+
}
216+
217+
TEST_CASE("task can co_yield with_stopped", "[types][task]")
218+
{
219+
bool destroyed = false;
220+
auto t = test_task_yields_stopped(destruction_probe{destroyed})
221+
| ex::upon_stopped(
222+
[&]() noexcept
223+
{
224+
CHECK(destroyed);
225+
});
226+
ex::sync_wait(std::move(t));
227+
CHECK(destroyed);
228+
}
229+
230+
auto test_task_returns_stopped([[maybe_unused]] destruction_probe probe) -> ex::task<int>
231+
{
232+
co_return ex::with_stopped();
233+
}
234+
235+
TEST_CASE("non-void task can co_return with_stopped", "[types][task]")
236+
{
237+
bool destroyed = false;
238+
auto t = test_task_returns_stopped(destruction_probe{destroyed})
239+
| ex::upon_stopped(
240+
[&]() noexcept
241+
{
242+
CHECK(destroyed);
243+
return 42;
244+
});
245+
auto [value] = ex::sync_wait(std::move(t)).value();
246+
CHECK(value == 42);
247+
CHECK(destroyed);
248+
}
249+
250+
# if !STDEXEC_NO_STDCPP_COROUTINE_RETURN_VOID_AND_VALUE()
251+
auto test_void_task_returns_stopped([[maybe_unused]] destruction_probe probe) -> ex::task<void>
252+
{
253+
co_return ex::with_stopped();
254+
}
255+
256+
TEST_CASE("void task can co_return with_stopped", "[types][task]")
257+
{
258+
bool destroyed = false;
259+
auto t = test_void_task_returns_stopped(destruction_probe{destroyed})
260+
| ex::upon_stopped(
261+
[&]() noexcept
262+
{
263+
CHECK(destroyed);
264+
});
265+
ex::sync_wait(std::move(t));
266+
CHECK(destroyed);
267+
}
268+
# endif
269+
270+
struct stopped_as_value
271+
{
272+
constexpr stopped_as_value(ex::with_stopped) noexcept {}
273+
};
274+
275+
auto test_task_returns_with_stopped_as_value() -> ex::task<stopped_as_value>
276+
{
277+
co_return ex::with_stopped();
278+
}
279+
280+
TEST_CASE("task returns with_stopped as a value when it is convertible to the value type",
281+
"[types][task]")
282+
{
283+
auto result = ex::sync_wait(test_task_returns_with_stopped_as_value());
284+
CHECK(result.has_value());
285+
}
286+
211287
# if !STDEXEC_NO_STDCPP_EXCEPTIONS()
212288
struct long_error_env
213289
{
@@ -239,8 +315,82 @@ namespace
239315
auto [error] = ex::sync_wait(test_task_catches_converted_yielded_error()).value();
240316
CHECK(error == 42);
241317
}
318+
319+
auto test_task_returns_convertible_error() -> ex::task<int, long_error_env>
320+
{
321+
co_return ex::with_error{42};
322+
}
323+
324+
auto test_task_catches_converted_returned_error() -> ex::task<long>
325+
{
326+
try
327+
{
328+
co_await test_task_returns_convertible_error();
329+
}
330+
catch (long error)
331+
{
332+
co_return error;
333+
}
334+
FAIL("Expected co_awaiting the task to throw its declared error type");
335+
co_return 0;
336+
}
337+
338+
TEST_CASE("non-void task converts a co_returned error to its declared error type",
339+
"[types][task]")
340+
{
341+
auto [error] = ex::sync_wait(test_task_catches_converted_returned_error()).value();
342+
CHECK(error == 42);
343+
}
344+
345+
# if !STDEXEC_NO_STDCPP_COROUTINE_RETURN_VOID_AND_VALUE()
346+
auto test_void_task_returns_convertible_error() -> ex::task<void, long_error_env>
347+
{
348+
co_return ex::with_error{42};
349+
}
350+
351+
auto test_task_catches_void_task_returned_error() -> ex::task<long>
352+
{
353+
try
354+
{
355+
co_await test_void_task_returns_convertible_error();
356+
}
357+
catch (long error)
358+
{
359+
co_return error;
360+
}
361+
FAIL("Expected co_awaiting the task to throw its declared error type");
362+
co_return 0;
363+
}
364+
365+
TEST_CASE("void task converts a co_returned error to its declared error type", "[types][task]")
366+
{
367+
auto [error] = ex::sync_wait(test_task_catches_void_task_returned_error()).value();
368+
CHECK(error == 42);
369+
}
370+
# endif
242371
# endif
243372

373+
struct error_as_value
374+
{
375+
constexpr error_as_value(ex::with_error<int> error) noexcept
376+
: value_(error.error)
377+
{}
378+
379+
int value_;
380+
};
381+
382+
auto test_task_returns_with_error_as_value() -> ex::task<error_as_value>
383+
{
384+
co_return ex::with_error{42};
385+
}
386+
387+
TEST_CASE("task returns with_error as a value when it is convertible to the value type",
388+
"[types][task]")
389+
{
390+
auto [value] = ex::sync_wait(test_task_returns_with_error_as_value()).value();
391+
CHECK(value.value_ == 42);
392+
}
393+
244394
// A sender type that does not claim to complete inline:
245395
struct just_int : ex::__result_of<ex::just, int>
246396
{

0 commit comments

Comments
 (0)