Document #: | P3325R4 |
Date: | 2024-11-21 |
Project: | Programming Language C++ |
Audience: |
LWG Library Evolution |
Reply-to: |
Eric Niebler <eric.niebler@gmail.com> |
“The environment is everything that isn’t me.”
— Albert
Einstein
Execution environments are a fundamental part of the sender-based
facilities in std::execution
,
but the current working draft provides no utility for creating or
manipulating environments. Such utilities are increasingly necessary
considering that some proposed APIs (e.g.,
write_env
from [P3284R0]) require passing an
environment.
This paper proposes two simple utilities for creating execution environments: one to create an environment out of a query/value pair and another to join several environments into one.
This paper proposes the following changes to the working draft:
Add a class template provisionally called
prop
to the
std::execution
namespace.
prop
associates a query
Q
with a value
V
, yielding a
queryable
object
E
such that
E.query(Q)
is equal to
V
.
Add a class template provisionally called
env
to the
std::execution
namespace.
env
aggregates several
environments into one, giving precedence to the environments in lexical
order.
Optionally, replace
empty_env
with
env<>
.
prop
and
env
.Change the target to the working draft instead of P2300.
For the type
prop<Query, Value>
mandate
that Query()(env)
is
well-formed, where env
is an
environment whose type is equivalent to
prop<Query, Value>
.
From the env
class
template, remove support for queries that take extra arguments.
Attributes [[nodiscard]]
and [[no_unique_address]]
are
removed from the specification, following LEWG’s and LWG’s
policies.
prop
or
env
should enforce the syntactic
requirements of the queries they support. See “Should Environments Be
Constrained?”.In [exec], execution environments are an internal implementation detail of the sender algorithms. There are no public-facing APIs that accept environment objects as a parameter. One may wonder why a utility for constructing environments is even necessary.
There are several reasons why the Standard Library would benefit from a utility to construct an environment:
The set of sender algorithms is openly extensible. For those who decide to implement their own sender algorithms, the manipulation of execution environments is part of the job. Standard utilities will make this easier.
[P3284R0] proposes a
write_env
sender adaptor that
merges a user-specified environment with the environment of the receiver
to which the sender is eventually connected. Using this adaptor requires
the user to construct an environment. Although implementing an
environment is not hard, an out-of-the-box solution will make using
write_env
simpler.
[P3149R3] proposes
two new algorithms, spawn
and
spawn_future
, both of which can
be parameterized by passing an environment as an optional
argument.
Currently, there is no way to parameterize algorithms like
sync_wait
and
start_detached
. The author
intends to bring a paper proposing overloads of those functions that
accept environments as a way to inject things like stop tokens,
allocators, and schedulers into the asynchronous operations that those
algorithms launch.
In short, environments are how users will inject dependencies into async computations. We can expect to see more APIs that will require (or will optionally allow) the user to pass an environment. Thus, a standard utility for making environments is desirable.
Defining an execution environment is quite simple. To build an environment with a single query/value pair, the following suffices:
template <class Query, class Value>
struct prop
{
[[no_unique_address]] Query query_;
Value value_;
auto query(Query) const noexcept -> const Value &
{
return value_;
}
};
// Example usage:
constexpr auto my_env = prop(get_allocator, std::allocator{});
Although simple, this template has some nice properties:
It is an aggregate so that members can be direct initialized from
temporaries without so much as a move. A function that constructs and
returns an environment using
prop
will benefit from RVO:
// The frobnicator object will be constructed directly in
// the callers stack frame:
return prop(get_frobnicator, make_a_frobnicator());
An object constructed like prop(get_allocator, my_allocator{})
will have the simple and unsurprising type prop<get_allocator_t, my_allocator>
.
The prop
utility proposed in
this paper is only slightly more elaborate than the
prop
class template shown above,
and it shares these properties.
What if you need to construct an environment with more than one
query/value pair, say, an allocator and a scheduler? A utility to join
multiple environments would work together with
prop
to make a general
environment-building utility. This paper calls that utility
env
.
The following code demonstrates:
template <class Env, class Query>
concept has-query
= requires (const Env& env) { env.query(Query()); };
template <class... Envs>
struct env : Envs...
{
template <class Query>
static constexpr size_t _index_of() {
constexpr bool flags[] = {has-query
<Envs, Query>...};
return ranges::find(flags, true) - flags;
}
template <class Query>
requires (has-query
<Envs, Query> ||...)
decltype(auto) query(Query q) const noexcept(...)
{
auto tup = tie(static_cast<const Envs&>(*this)...);
return get<_index_of<Query>()>(tup).query(q);
}
};
template <class... Envs>
(Envs...) -> env<Envs...>;
env
// Example usage:
constexpr auto my_env = env{prop(get_allocator, my_alloc{}),
(get_scheduler, my_sched{})}; prop
This env
class template
shares the desirable properties of
prop
: aggregate initialization
and unsprising naming. A query is handled by the “leftmost” child
environment that can handle it.
Note that the above code has the issue that two child environments cannot have the same type. That is not a limitation of the facility proposed below.
There are times when you would like an environment to respond to a query with a reference to a particular object rather than a copy. Capturing a reference is dangerous, so the opt-in to reference semantics should be explicit.
std::reference_wrapper
is how
the Standard Library deals with such problems. It should be possible to
construct an environment using
std::ref
to specify that the
“value” should be stored by reference:
::mutex mtx;
stdconst auto my_env = prop(get_mutex, std::ref(mtx));
::mutex & ref = get_mutex(my_env);
stdassert(&ref == &mtx);
Similarly, there are times when you would like to store the child
environment by reference.
std::ref
should work for that
case as well:
// At namespace scope, construct a reusable environment:
constexpr auto global_env = prop(get_frobnicator, make_frobnicator());
// Construct an environment with a particular value for the get_allocator
// query, and a reference to the constexpr global_env:
auto env_with_allocator(auto alloc)
{
return env{prop(get_allocator, alloc), std::ref(global_env)};
}
The utility proposed in this paper satisfies all of the above use cases.
An interesting question came up during LEWG design review at the
St. Louis committee meeting. Consider a
queryable
object
e
such that
e.query(get_scheduler)
is
well-formed. The get_scheduler
query should always return an object whose type satisfies the
scheduler
concept. So, should
the expression
e.query(get_scheduler)
be
somehow constrained with that requirement? Should the expression be
ill-formed if it returns an int
,
say.
During the LEWG telecon on July 16, 2024, it was decided that
instantiations of the
prop<Query, Value>
class
template should mandate that
Query
is callable with
prop<Query, Value>
(or
rather, a type equivalent to
prop<Query, Value>
since
the prop<Query, Value>
type will be incomplete at the time the requirement is checked).
This will have the effect that, if
Query()(env)
has requirements or
mandates on the expression
env.query(Query())
, that those
requirements or mandates will cause a hard error when they are not met,
and that the error happens as early as possible: when the
prop
class template is
instantiated.
For example, consider the following query:
struct get_scheduler_t
{
template <class Env>
requires has-query
<Env, get_scheduler_t>
decltype(auto) operator()(const Env& env) const
{
// Mandates: env.query(*this) returns a scheduler
static_assert(
<decltype(env.query(*this))>,
scheduler"The 'get_scheduler' query must return a type that satisfies the 'scheduler' concept.");
return env.query(*this);
}
}
inline constexpr get_scheduler_t get_scheduler {};
With the code above, the expression
prop(get_scheduler, 42)
will
cause the program to be ill-formed because the type
int
does not satisfy the
scheduler
concept.
For the prop
utility, a
couple of other plausible names come to mind:
attr
with
property
key_value
query_value
Other possibile names for env
are:
attributes
, or
attrs
properties
, or
props
dictionary
, or
dict
Types with a very similar designs have seen heavy use in
stdexec
for over two years. The
design proposed below has been prototyped and can be found on Compiler Explorer1.
A note on implementability: The implementation of
env
is simple if we only want to
support braced list initialization with class template argument
deduction, like env{e1, e2, e2}
.
Initialization from a parenthesized expression list with CTAD for an
arbitrary number of arguments (e.g.,
env(e1, e2, e3, ...)
) is not
implementable in standard C++ to the author’s knowledge.
In the proposed wording, env
is specified such that it will be able to benefit from member packs
([P3115R0]) if and when they become
available or should aggregate initialization with a parenthesized
expression list ever be extended to support brace elision for
subobjects.
There are several ways this utility might be extended in the future:
Replace all occurances of
empty_env
with
env<>
.
[ Editor's note: Change [exec.syn] as follows: ]
Header
<execution>
synopsis [exec.syn]namespace std::execution { …as before…
struct get_env_t { see below }; inline constexpr get_env_t get_env{}; template<class T> using env_of_t = decltype(get_env(declval<T>()));struct empty_env {};
// [exec.prop] class template prop
template<class QueryTag, class ValueType>
struct prop;
// [exec.env] class template env
template<queryable... Envs>
struct env;
// [exec.domain.default], execution domains struct default_domain; // [exec.sched], schedulers struct scheduler_t {}; …as before… }
[ Editor's note: After [exec.utils], add a new subsection [exec.envs] as follows: ]
34.12 Queryable utilities [exec.envs]
34.12.1 Class template
prop
[exec.prop]namespace std::execution { template<class QueryTag, class ValueType> struct prop {
query_
; // exposition only QueryTagvalue_
; // exposition only ValueType constexpr const ValueType& query(QueryTag) const noexcept {value_
; return } }; template<class QueryTag, class ValueType> prop(QueryTag, ValueType) -> prop<QueryTag, unwrap_reference_t<ValueType>>; }
Class template
prop
is for building a queryable object from a query object and a value.Mandates:
callable<QueryTag, prop-like<ValueType>>
, whereprop-like
is the following exposition-only class template:template<class ValueType> struct prop-like { // exposition only const ValueType& query(auto) const noexcept; };
- [Example 1:
template<sender Sndr> sender auto parameterize_work(Sndr sndr) {
// Make an environment such that get_allocator(env) returns a reference to a copy
// of my_alloc{}
auto e = prop(get_allocator, my_alloc{});// parameterize the input sender so that it will use our custom execution environment
return write_env(sndr, e); }— end example]
- Specializations of
prop
are not assignable.34.12.2 Class template
env
[exec.env]namespace std::execution {
queryable
... Envs> template< struct env {envs_
0; // exposition only Envs0envs_
1; // exposition only Envs1 ...envs_
n-1; // exposition only Envsn-1 template<class QueryTag> constexpr decltype(auto) query(QueryTag q) const noexcept(see below); }; template<class... Envs> env(Envs...) -> env<unwrap_reference_t<Envs>...>; }
The class template
env
is used to construct a queryable object from several queryable objects. Query invocations on the resulting object are resolved by attempting to query each subobject in lexical order.Specializations of
env
are not assignable.It is unspecified whether
env
supports class template argument deduction ([over.match.class.deduct]) when performing initialization using a parenthesized expression-list ([dcl.init]).[Example 1:
template<sender Sndr> sender auto parameterize_work(Sndr sndr) {
// Make an environment such that:
// - get_allocator(env) returns a reference to a copy of my_alloc{}
// - get_scheduler(env) returns a reference to a copy of my_sched{}
auto e = env{prop(get_allocator, my_alloc{}), prop(get_scheduler, my_sched{})};// parameterize the input sender so that it will use our custom execution environment
return write_env(sndr, e); }— end example]
34.12.2.1
env
members [exec.env.members]template<class QueryTag> constexpr decltype(auto) query(QueryTag q) const noexcept(see below);
- Let
has-query
be the following exposition-only concept:template<class Env, class QueryTag> concept has-query = // exposition only requires (const Env& env) { env.query(QueryTag()); };
Let
fe
be the first element ofsuch that the expression
envs_
0,envs_
1, ...envs_
n-1fe.query(q)
is well-formed.Requires:
(has-query<Envs, QueryTag> || ...)
istrue
.Effects: Equivalent to
fe.query(q)
.Remarks: The expression in the
noexcept
clause is equivalent tonoexcept(fe.query(q))
.
I would like to thank Lewis Baker for many enlightening conversations about the design of execution environments. The design presented here owes much to Lewis’s insights.