Skip to content

Commit dd6be78

Browse files
committed
Added support for cancellable raw senders with a lambda body
1 parent 07bafa7 commit dd6be78

4 files changed

Lines changed: 297 additions & 35 deletions

File tree

doc/api_reference.md

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,9 @@ callable is passed an rvalue reference to receiver as an argument and is expecte
274274
move-construct the receiver inside the returned object.
275275
276276
The operation state returned by the factory should either define a method `void
277-
start() noexcept` or be a non-throwing callable.
277+
start() noexcept` or be a non-throwing callable. For senders wrapped into `cancellable`
278+
to implement cancellation, there are additional requirements on the operation state
279+
(see [`cancellable`](#cancellablesender-sender-stdtrue_type---sender)).
278280
279281
`ValueTypes...` is a pack representing the value types of the resulting sender. The
280282
user is expected to call `unifex::set_value()` passing in the rvalue to preserved
@@ -501,6 +503,41 @@ controlling what happens when stop is requested before the operation is started:
501503
If the receiver does not support cancellation, no stop callback is
502504
registered and the operation runs unconditionally without stop overhead.
503505
506+
#### Implementing operation state with a lambda
507+
508+
The combination of `cancellable` and `create_raw_sender()` can be used with a
509+
lambda as operation state. Such a lambda must accept a generic (`auto`) event
510+
argument used to separate `start()` and `stop()` invocations into template
511+
instantiations with the alternate branch of the code optimized away with `if constexpr`:
512+
513+
```c++
514+
auto makeMySender(StateArg arg) {
515+
return cancellable(
516+
create_raw_sender<ResultType>(
517+
[arg](auto&& receiver) {
518+
return [receiver = std::move(receiver), arg,
519+
requestId = RequestId{}](auto event, auto* self) mutable {
520+
if constexpr (event.is_start) {
521+
requestId = async_request(
522+
[self, &receiver](ResultType result) {
523+
if (try_complete(self)) {
524+
unifex::set_value(std::move(receiver), result);
525+
}
526+
},
527+
arg);
528+
} else if constexpr (event.is_stop) {
529+
if (try_complete(self)) {
530+
cancel_async_request(requestId);
531+
}
532+
}
533+
};
534+
}));
535+
}
536+
```
537+
538+
The `self` argument is what the implementation is to pass to `try_complete()`. It does
539+
not point directly to the lambda closure and is of a different pointer type.
540+
504541
### `then(Sender predecessor, Func func) -> Sender`
505542

506543
Returns a sender that transforms the value of the `predecessor` by calling

include/unifex/create_raw_sender.hpp

Lines changed: 16 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <unifex/sender_concepts.hpp>
2020
#include <unifex/type_traits.hpp>
2121

22+
#include <unifex/detail/lambda_op.hpp>
2223
#include <unifex/detail/make_traits.hpp>
2324
#include <unifex/detail/prologue.hpp>
2425

@@ -73,27 +74,6 @@ constexpr _make_traits::get_traits<_make_traits::sender_traits_literal>::type<
7374

7475
namespace _create_raw_sndr {
7576

76-
template <typename Receiver, typename Fn, typename... ValueTypes>
77-
struct _op {
78-
static_assert(std::is_invocable_v<Fn, Receiver&&>);
79-
using state_t = decltype(UNIFEX_DECLVAL(Fn)(UNIFEX_DECLVAL(Receiver)));
80-
static_assert(std::is_nothrow_invocable_v<state_t>);
81-
82-
struct type {
83-
explicit type(Receiver&& rec, Fn&& fn) noexcept(
84-
std::is_nothrow_invocable_v<Fn, Receiver&&>)
85-
: state_(fn(std::forward<Receiver>(rec))) {}
86-
87-
void start() noexcept { state_(); }
88-
89-
private:
90-
UNIFEX_NO_UNIQUE_ADDRESS state_t state_;
91-
};
92-
};
93-
94-
template <typename Receiver, typename Fn, typename... ValueTypes>
95-
using _op_wrapper = typename _op<Receiver, Fn, ValueTypes...>::type;
96-
9777
template <typename Tr, typename Fn, typename... ValueTypes>
9878
struct _sender : public Tr {
9979
template <
@@ -107,30 +87,32 @@ struct _sender : public Tr {
10787
explicit _sender(Fn&& fn) noexcept(std::is_nothrow_move_constructible_v<Fn>)
10888
: fn_(std::forward<Fn>(fn)) {}
10989

90+
// Overload 1: factory returns a type with start(). Returned directly.
11091
template <typename Receiver>
11192
requires receiver_of<Receiver, ValueTypes...> &&
112-
(!std::is_invocable_v<
113-
_start_cpo::_fn,
114-
decltype(UNIFEX_DECLVAL(Fn)(UNIFEX_DECLVAL(Receiver)))&>)
93+
std::is_invocable_v<
94+
_start_cpo::_fn,
95+
decltype(UNIFEX_DECLVAL(Fn)(UNIFEX_DECLVAL(Receiver)))&>
11596
friend auto
11697
tag_invoke(tag_t<connect>, _sender&& self, Receiver&& rec) noexcept(
117-
std::is_nothrow_constructible_v<
118-
_op_wrapper<Receiver, Fn, ValueTypes...>,
119-
Receiver&&,
120-
Fn&&>) {
121-
return _op_wrapper<Receiver, Fn, ValueTypes...>{
122-
std::forward<Receiver>(rec), std::move(self.fn_)};
98+
std::is_nothrow_invocable_v<Fn, Receiver&&>) {
99+
return std::move(self.fn_)(std::forward<Receiver>(rec));
123100
}
124101

102+
// Overload 2: factory returns a callable (no start()). Wrapped in
103+
// _sender_op::_op which auto-selects the right specialization:
104+
// plain callable → start() only; event-dispatch → start() + stop().
125105
template <typename Receiver>
126106
requires receiver_of<Receiver, ValueTypes...> &&
127-
std::is_invocable_v<
128-
_start_cpo::_fn,
129-
decltype(UNIFEX_DECLVAL(Fn)(UNIFEX_DECLVAL(Receiver)))&>
107+
(!std::is_invocable_v<
108+
_start_cpo::_fn,
109+
decltype(UNIFEX_DECLVAL(Fn)(UNIFEX_DECLVAL(Receiver)))&>)
130110
friend auto
131111
tag_invoke(tag_t<connect>, _sender&& self, Receiver&& rec) noexcept(
132112
std::is_nothrow_invocable_v<Fn, Receiver&&>) {
133-
return std::move(self.fn_)(std::forward<Receiver>(rec));
113+
using state_t = decltype(std::move(self.fn_)(std::forward<Receiver>(rec)));
114+
return _lambda_op::_op<state_t>{
115+
std::move(self.fn_)(std::forward<Receiver>(rec))};
134116
}
135117

136118
UNIFEX_NO_UNIQUE_ADDRESS Fn fn_;
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
#pragma once
17+
18+
#include <unifex/type_traits.hpp>
19+
20+
#include <type_traits>
21+
22+
namespace unifex::_lambda_op {
23+
24+
// Probe types for detecting event-dispatch lambdas.
25+
// A lambda (auto event, auto* self) that branches on event.is_start /
26+
// event.is_stop is callable with these probes.
27+
struct _start_probe {
28+
static constexpr bool is_start = true;
29+
static constexpr bool is_stop = false;
30+
};
31+
32+
struct _stop_probe {
33+
static constexpr bool is_start = false;
34+
static constexpr bool is_stop = true;
35+
};
36+
37+
// Detection: a callable is event-dispatchable if it is not callable
38+
// with zero arguments but IS callable with start/stop probes.
39+
// The 2-arg check uses the probe types themselves as the self-pointer
40+
// type (not void*) to avoid try_complete<void> instantiation failures
41+
// when is_invocable_v triggers body instantiation for return-type
42+
// deduction on generic lambdas.
43+
template <typename T>
44+
inline constexpr bool _is_event_dispatchable_v = !std::is_invocable_v<T&> &&
45+
(std::is_invocable_v<T&, _start_probe> ||
46+
std::is_invocable_v<T&, _start_probe, _start_probe*>) &&
47+
(std::is_invocable_v<T&, _stop_probe> ||
48+
std::is_invocable_v<T&, _stop_probe, _stop_probe*>);
49+
50+
// Unified operation state aggregate for callables returned by
51+
// create_raw_sender factories. Wraps a callable with start() (and
52+
// stop() for event-dispatch lambdas). Being an aggregate, supports
53+
// non-moveable callables (e.g. lambdas capturing std::atomic) when
54+
// constructed from a prvalue via guaranteed copy elision.
55+
//
56+
// The IsEventDispatch parameter is auto-deduced from the callable type.
57+
template <
58+
typename Callable,
59+
bool IsEventDispatch = _is_event_dispatchable_v<Callable>>
60+
struct _op;
61+
62+
// Plain callable: operator()() → start()
63+
template <typename Callable>
64+
struct _op<Callable, false> {
65+
UNIFEX_NO_UNIQUE_ADDRESS Callable callable_;
66+
void start() noexcept { callable_(); }
67+
};
68+
69+
// Event-dispatch lambda: operator()(event[, self*]) → start() + stop()
70+
template <typename Lambda>
71+
struct _op<Lambda, true> {
72+
Lambda lambda_;
73+
74+
void start() noexcept {
75+
if constexpr (std::is_invocable_v<Lambda&, _start_probe, _op*>) {
76+
lambda_(_start_probe{}, this);
77+
} else {
78+
lambda_(_start_probe{});
79+
}
80+
}
81+
82+
void stop() noexcept {
83+
if constexpr (std::is_invocable_v<Lambda&, _stop_probe, _op*>) {
84+
lambda_(_stop_probe{}, this);
85+
} else {
86+
lambda_(_stop_probe{});
87+
}
88+
}
89+
};
90+
91+
} // namespace unifex::_lambda_op

test/cancellable_test.cpp

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,158 @@ TEST(cancellable_test, with_query_value_stoppable_token) {
225225
EXPECT_FALSE(stopped);
226226
}
227227

228+
TEST(cancellable_lambda_test, completes_synchronously) {
229+
auto result{sync_wait(cancellable{create_raw_sender<int>([](auto&& receiver) {
230+
return [receiver = std::forward<decltype(receiver)>(receiver)](
231+
auto event, auto* self) mutable {
232+
if constexpr (event.is_start) {
233+
if (try_complete(self)) {
234+
set_value(std::move(receiver), 42);
235+
}
236+
}
237+
};
238+
})})};
239+
EXPECT_TRUE(result.has_value());
240+
EXPECT_EQ(*result, 42);
241+
}
242+
243+
// Note: stop_while_running with stop_when is tested by the struct-based
244+
// test above. The equivalent lambda version triggers an MSVC internal
245+
// compiler error due to excessive template nesting depth (stop_when +
246+
// cancellable + create_raw_sender + lambda). The stop mechanism with
247+
// event-dispatch lambdas is covered by stops_early and
248+
// completes_before_stop_is_forwarded below.
249+
250+
TEST(cancellable_lambda_test, stops_early) {
251+
bool started = false;
252+
bool stopped = false;
253+
auto result{sync_wait(let_value_with_stop_source([&](auto& stop_src) {
254+
stop_src.request_stop();
255+
return cancellable{
256+
create_raw_sender<int>([&](auto&& receiver) {
257+
return [receiver = std::forward<decltype(receiver)>(receiver),
258+
&started,
259+
&stopped](auto event, auto* self) mutable {
260+
if constexpr (event.is_start) {
261+
started = true;
262+
if (try_complete(self)) {
263+
set_value(std::move(receiver), 42);
264+
}
265+
} else if constexpr (event.is_stop) {
266+
stopped = true;
267+
if (try_complete(self)) {
268+
set_done(std::move(receiver));
269+
}
270+
}
271+
};
272+
}),
273+
std::true_type{}};
274+
}))};
275+
EXPECT_FALSE(result.has_value());
276+
EXPECT_FALSE(started);
277+
EXPECT_TRUE(stopped);
278+
}
279+
280+
TEST(cancellable_lambda_test, completes_before_stop_is_forwarded) {
281+
bool started = false;
282+
bool stopped = false;
283+
auto result{sync_wait(let_value_with_stop_source([&](auto& stop_src) {
284+
stop_src.request_stop();
285+
return cancellable{create_raw_sender<int>([&](auto&& receiver) {
286+
return [receiver = std::forward<decltype(receiver)>(receiver),
287+
&started,
288+
&stopped](auto event, auto* self) mutable {
289+
if constexpr (event.is_start) {
290+
started = true;
291+
if (try_complete(self)) {
292+
set_value(std::move(receiver), 42);
293+
}
294+
} else if constexpr (event.is_stop) {
295+
stopped = true;
296+
if (try_complete(self)) {
297+
set_done(std::move(receiver));
298+
}
299+
}
300+
};
301+
})};
302+
}))};
303+
EXPECT_TRUE(result.has_value());
304+
EXPECT_EQ(*result, 42);
305+
EXPECT_TRUE(started);
306+
EXPECT_FALSE(stopped);
307+
}
308+
309+
TEST(cancellable_lambda_test, without_self_pointer) {
310+
// Lambda taking only (event) without self pointer.
311+
// Useful when try_complete is not needed (e.g. unconditional
312+
// synchronous completion with unstoppable token).
313+
auto result{sync_wait(with_query_value(
314+
cancellable{create_raw_sender<int>([](auto&& receiver) {
315+
return [receiver = std::forward<decltype(receiver)>(receiver)](
316+
auto event) mutable {
317+
if constexpr (event.is_start) {
318+
set_value(std::move(receiver), 99);
319+
}
320+
};
321+
})},
322+
get_stop_token,
323+
unstoppable_token{}))};
324+
EXPECT_TRUE(result.has_value());
325+
EXPECT_EQ(*result, 99);
326+
}
327+
328+
TEST(cancellable_lambda_test, with_non_moveable_state) {
329+
// Verifies that event-dispatch lambdas capturing non-moveable types
330+
// (std::atomic) work via aggregate init from prvalue in _event_op.
331+
auto result{sync_wait(with_query_value(
332+
cancellable{create_raw_sender<int>([](auto&& receiver) {
333+
return
334+
[receiver = std::forward<decltype(receiver)>(receiver),
335+
call_count = std::atomic<int>{0}](auto event, auto* self) mutable {
336+
if constexpr (event.is_start) {
337+
call_count.fetch_add(1, std::memory_order_relaxed);
338+
if (try_complete(self)) {
339+
set_value(
340+
std::move(receiver),
341+
call_count.load(std::memory_order_relaxed));
342+
}
343+
}
344+
};
345+
})},
346+
get_stop_token,
347+
unstoppable_token{}))};
348+
EXPECT_TRUE(result.has_value());
349+
EXPECT_EQ(*result, 1);
350+
}
351+
352+
TEST(cancellable_lambda_test, with_unstoppable_token) {
353+
auto result{sync_wait(with_query_value(
354+
cancellable{create_raw_sender<int>([](auto&& receiver) {
355+
return [receiver = std::forward<decltype(receiver)>(receiver)](
356+
auto event, auto* self) mutable {
357+
if constexpr (event.is_start) {
358+
if (try_complete(self)) {
359+
set_value(std::move(receiver), 42);
360+
}
361+
}
362+
};
363+
})},
364+
get_stop_token,
365+
unstoppable_token{}))};
366+
EXPECT_TRUE(result.has_value());
367+
EXPECT_EQ(*result, 42);
368+
}
369+
370+
TEST(cancellable_lambda_test, event_probe_fields) {
371+
// Verify probe types have the expected boolean fields.
372+
// The lambda's if-constexpr dispatch depends on them.
373+
using namespace unifex::_lambda_op;
374+
static_assert(_start_probe::is_start);
375+
static_assert(!_start_probe::is_stop);
376+
static_assert(!_stop_probe::is_start);
377+
static_assert(_stop_probe::is_stop);
378+
}
379+
228380
#endif
229381

230382
TEST(cancellable_test, constructs_sender_in_place) {

0 commit comments

Comments
 (0)