write_env
and unstoppable
Sender
AdaptorsDocument #: | P3284R2 |
Date: | 2024-11-21 |
Project: | Programming Language C++ |
Audience: |
LEWG Library Evolution |
Reply-to: |
Eric Niebler <eric.niebler@gmail.com> |
This paper proposes to add two new sender adaptor algorithms to the
std::execution
namespace,
targetting C++26: write_env
and
unstoppable
. These adaptors were
originally proposed as part of [P3175R3] but were split out into their
own paper so that the higher priority items in P3175 could advance more
quickly.
Below are the specific changes this paper proposes:
Add a new uncustomizable adaptor
write_env
for writing values
into the receiver’s execution environment. Use
write_env
in the implementation
of the on
algorithm and to
simplify the specification of the
let_
algorithms.
Add an uncustomizable
unstoppable
adaptor that is a
trivial application of
write_env
: it sets the current
stop token in the receiver’s environment to a
never_stop_token
.
[P3175R3] proposed some changes to the
std::execution::on
algorithm,
the specification of which was made simpler by the addition of some
additional adaptors. Those adaptors were general and useful in their own
right, so P3175R3 suggested they be added to
std::execution
proper. The
conservative approach was to make them exposition-only, and that is how
things currently stand in the working draft.
The author still feels like those adaptors are worthy of standardization. This paper proposes adding them.
The adaptors in question are as follows:
write_env
A receiver has an associated “execution environment”, which is an
unstructured, queryable key/value store. It is used to pass implicit
parameters from parent operations to their children. It is occasionally
useful for a sender adaptor to explicitly mutate the key/value store so
that child operations see different values for environment queries. The
write_env
sender adaptor is used
for that purpose.
write_env
is a customization
point object, although it is not actually customizable. It accepts a
sender sndr
and an execution
environment env
, and it returns
a new sender that stores sndr
and env
. When that sender is
connected to a receiver rcvr
, it
returns the result of connecting
sndr
with a receiver that adapts
rcvr
. The environment of that
adapted receiver is the result of joining
env
with
rcvr
’s environment. The two
environments are joined such that, when the joined environment is
queried, env
is queried first,
and if env
doesn’t have a value
for that query, the result of
get_env(rcvr)
is queried.
write_env
One example of where
write_env
might be useful is to
specify an allocator to be used by child operations. The code might look
like this:
// Turn a query object and a value into a queryable environment
// (see [@P3325R2]):
template <class Query, class Value>
struct prop {
Query query;
Value value;decltype(auto) query(Query) const noexcept { return (value); }
};
// Adapts a sender so that it can use the given allocator:
struct with_allocator_t {
template <std::execution::sender Sndr, class Alloc>
auto operator()(Sndr sndr, Alloc alloc) const {
return std::execution::write_env(sndr, prop(std::get_allocator, alloc));
}
template <class Alloc>
auto operator()(Alloc alloc) const {
return std::execution::write_env(prop(std::get_allocator, alloc));
}
};
constexpr with_allocator_t with_allocator{};
The with_allocator
adaptor
might be used to parameterize senders produced by a third-party library
as follows:
namespace ex = std::execution;
// This returns a sender that does some piece of asynchronous work
// created by a third-party library, but parameterized with a custom
// allocator.
::sender auto make_async_work_with_alloc() {
ex::sender auto work = third_party::make_async_work();
ex
return with_allocator(std::move(work), custom_allocator());
}
The sender returned by
third_party::make_async_work
might query for the allocator and use it to do allocations:
namespace third_party {
namespace ex = std::execution;
// A function that returns a sender that generates data on a special
// execution context, populate a std::vector with it, and then completes
// by sending the vector.
constexpr auto _populate_data_vector =
[]<class Allocator>(Allocator alloc) {
// Create an empty vector of ints that uses a specified allocator.
using IntAlloc = std::allocator_traits<Allocator>::template rebind_alloc<int>;
auto data = std::vector<int, IntAlloc>{IntAlloc{std::move(alloc)}};
// Create some work that generates data and fills in the vector.
auto work = ex::just(std::move(data))
| ex::then([](auto data) {
// Generate the data and fill in the vector:
.append_range(third_party::make_data())
datareturn data;
});
// Execute the work on a special third_party execution context:
// (This uses the `on` as specified in P3175.)
return ex::on(third_party_scheduler(), std::move(work));
};
// A function that returns the sender produced by `_populate_data_vector`,
// parameterized by an allocator read out of the receiver's environment.
::sender auto make_async_work() {
exreturn ex::let_value(
// This reads the allocator out of the receiver's execution environment.
::read_env(std::get_allocator),
ex
_populate_data_vector);
}
}
unstoppable
The unstoppable
sender
adaptor is a trivial application of
write_env
that modifies a sender
so that it no longer responds to external stop requests. That can be of
critical importance when the successful completion of a sender is
necessary to ensure program correctness, e.g., to restore an
invariant.
The unstoppable
adaptor might
be implemented as follows:
inline constexpr struct unstoppable-t {
template <sender Sndr>
auto operator()(Sndr sndr) const {
return write_env(std::move(sndr), prop(std::get_stop_token, never_stop_token()));
}
auto operator()() const {
return write_env(prop(std::get_stop_token, never_stop_token()));
}
} unstoppable {};
unstoppable
In the following example, some asynchronous work must temporarily
break a program invariant. It uses
unstoppable
and a hypothetical
finally
algorithm to restore the
invariant. finally
runs a
predecessor sender, saves its results, runs another sender, and then
propagates saved results of the predecessor.
namespace ex = std::execution;
::sender auto break_invariants(auto&... values);
ex::sender auto restore_invariants(auto&... values);
ex
// This function returns a sender adaptor closure object. When applied to
// a sender, it returns a new sender that breaks program invariants,
// munges the data, and restores the invariants.
auto safely_munge_data( ) {
return ex::let_value( [](auto&... values) {
return break_invariants(values...)
| ex::then(do_munge) // the invariants will be restored even if `do_munge` throws
| finally(ex::unstoppable(restore_invariants(values...)));
} );
}
auto sndr = ...;
( sndr | safely_munge_data(), scope_token ); // See `counting_scope` from P3149R6 spawn
[ Editor's note: The wording in this section is based on the current working draft. ]
[ Editor's note: Change [exec.syn] as follows: ]
inline constexpr unspecified write_env{};
inline constexpr unspecified unstoppable{};
inline constexpr start_on_t start_on{};
inline constexpr continue_on_t continue_on{};
inline constexpr on_t on{}; inline constexpr schedule_from_t schedule_from{};
[ Editor's note: Replace
all instances of
“write-env
” with
“write_env
”. After
[exec.adapt.objects], add a new subsection
“execution::write_env
[exec.write.env]” and move the specification of the exposition-only
write-env
from
[exec.snd.expos]/p40-43 into it with the following modifications:
]
(34.9.11.?)
execution::write_env
[exec.write.env]
write-env
write_env
is rcvr
, connects the
adapted sender with a receiver whose execution environment is the result
of joining the queryable
env
get_env(rcvr)
.Let write-env-t
be
an exposition-only empty class type.
Returns: make-sender(make-env-t(), std::forward<Env>(env), std::forward<Sndr>(sndr))
.
write_env
is a customization
point object. For some subexpressions
sndr
and
env
, if
decltype((sndr))
does not
satisfy sender
or if
decltype((env))
does not satisfy
queryable
, the
expression write_env(sndr, env)
is ill-formed. Otherwise, it is expression-equivalent to
make-sender(write_env, env, sndr)
.Remarks: The
exposition-only class template
impls-for
([exec.snd.expos]) is specialized for write-env-t
write_env
as follows:
template<>write-env-t
decayed-typeof<write_env>
> : default-impls {
struct impls-for<
static constexpr auto get-env =
[](auto, const auto& env, const auto& rcvr) noexcept {
return JOIN-ENV(env, get_env(rcvr));
}; };
[ Editor's note: After
[exec.write.env], add a new subsection
“execution::unstoppable
[exec.unstoppable]” as follows: ]
(34.9.11.?)
execution::unstoppable
[exec.unstoppable]
unstoppable
is a sender
adaptor that connects its inner sender with a receiver that has the
execution environment of the outer receiver but with a
never_stop_token
as the value of
the get_stop_token
query.
For a subexpression sndr
,
unstoppable(sndr)
is expression
equivalent to write_env(sndr, MAKE-ENV(get_stop_token,
never_stop_token{}))
.