scope_association
concept to
P3149Document #: | P3815R0 |
Date: | 2025-09-01 |
Project: | Programming Language C++ |
Audience: |
LEWG Library Evolution Working Group |
Reply-to: |
Ian Petersen <ispeters@gmail.com> Jessica Wong <jesswong2011@gmail.com> |
[P3149R11] was approved for C++26 by
WG21. The paper introduces two scope types,
simple_counting_scope
and
counting_scope
, along with
several basis operations, including
associate
,
spawn
, and
spawn_future
. In R11, the
example implementations of these facilities are expressed in terms of
the scope_token
concept:
template <class Token>
concept scope_token =
<Token> &&
copyablerequires(const Token token) {
{ token.try_associate() } -> same_as<bool>;
{ token.disassociate() } noexcept -> same_as<void>;
{ token.wrap(declval<test-sender>()) } -> sender_in<test-env>;
};
During the development of these sample implementations, it was
observed that reintroducing the
scope_association
concept from
[P3149R7] would yield several
benefits:
These improvements can be achieved without any impact on the user-facing APIs proposed in R11.
Illustrated below are the R11 implementations of
spawn
and
associate
in contrast with the
scope_association
concept
implementation.
Before
|
After
|
---|---|
|
|
Before
|
After
|
---|---|
|
|
template <class Assoc>
concept scope_association =
<Assoc> &&
movable<Assoc> &&
default_initializablerequires(Assoc assoc) {
{ static_cast<bool>(assoc) } noexcept;
{ assoc.try_associate() } -> same_as<Assoc>;
};
A type that models
scope_association
is an RAII
handle that represents a possible association between a sender and an
async scope. If the scope association contextually converts to true then
the object is “engaged” and represents an association; otherwise, the
object is “disengaged” and represents the lack of an association. Scope
associations are movable and not copyable, and expose a
try_associate
member function
with semantics identical to the
try_associate
member function on
a type that models
scope_token
.
The following are the proposed changes to
scope_token
,
associate
,
spawn
,
spawn_future
,
simple_counting_scope
, and
counting_scope
with the adoption
of scope_association
.
execution::scope_token
The primary change to
scope_token
is to
try_associate
, which will return
a scope_association
rather than
a bool
.
template <class Token>
concept scope_token =
<Token> &&
copyablerequires(Token token) {
{ token.try_associate() } -> scope_association;
{ token.wrap(declval<test-sender>()) } -> sender_in<test-env>;
};
The try_associate
member
function on a token attempts to create a new association with the scope;
try_associate
returns an engaged
association when the association is successful, and it may either return
a disengaged association or throw an exception to indicate failure.
execution::associate
With the application of the proposed changes, the copy behavior of
the associate-sender returned from
associate
becomes the
following:
If the sender, snd
, provided
to associate()
is copyable then
the resulting associate-sender is also copyable, with the following
rules:
copying an unassociated associate-sender invariably produces a new unassociated associate-sender; and
copying an associated associate-sender requires copying the
associate-data
it
contains and the
associate-data
copy-constructor proceeds as follows:
The result of invoking the source’s
association.try_associate()
will be passed to the destination
associate-data
.
associate-data
; the
destination associate-sender is associatedFurthermore, the
operation-state
’s
destructor becomes the following:
An operation-state
with its own association must invoke the association’s destructor as the
last step of the
operation-state
’s
destructor.
execution::spawn
The behavior of spawn
remains
largely unchanged, with the primary difference being that
op_t
now holds an
association
rather than
a token
. Upon
completion of the
operation-state
, the
destructor of the
association
is invoked,
replacing the previous mechanism of explicitly calling
token.disassociate()
on
the local copy of the
token
.
execution::spawn_future
The changes to spawn_future
reflect the same changes proposed in
spawn
.
execution::simple_counting_scope
The behavior of
simple_counting_scope
remains
largely unchanged, with the primary difference being that the
disassociation is handled by the destructor of the association returned
from token.try_associate()
.
execution::counting_scope
The changes to counting_scope
reflect the same changes proposed in
simple_counting_scope
.
<execution>
synopsis
33.4
[execution.syn]To the <execution>
synopsis 33.4
[execution.syn],
make the following change:
// [exec.scope]
// [exec.scope.concepts], scope conceptstemplate <class Token>
concept scope_association = see below;
template <class Token> concept scope_token = see below;
execution::associate
To the subsection 33.9.12.16 [exec.associate], make the following changes:
2
Let associate-data
be
the following exposition-only class template:
namespace std::execution {
template <scope_token Token, sender Sender>
struct associate-data { // exposition only
using wrap-sender = // exposition only
remove_cvref_t<decltype(declval<Token&>().wrap(declval<Sender>()))>;using assoc-t = // exposition only
decltype(declval<Token&>().try_associate());
using sender-ref = // exposition only
unique_ptr<wrap-sender, decltype([](auto* p) noexcept { destroy_at(p); })>;
explicit associate-data(Token t, Sender&& s), {
: sndr(t.wrap(std::forward<Sender>(s)))token(t) {
sender-ref guard{addressof(sndr)};
!tokenassoc = t.try_associate())
if (sndr.reset()(void)guard.release();
}
associate-data(const associate-data& other)
noexcept(is_nothrow_copy_constructible_v<wrap-sender> &&tokenassoc.try_associate()));
noexcept(other.
associate-data(associate-data&& other)
noexcept(is_nothrow_move_constructible_v<wrap-sender>);
~associate-data();
optional<pair<Token, wrap-sender>>
pair<assoc-t, sender-ref>
(is_nothrow_move_constructible_v<wrap-sender>);
release() && noexcept
private:optional<wrap-sender> sndr; // exposition only
Token token; // exposition only
associate-data(pair<assoc-t, scope-ref> parts); // exposition only
assoc-t assoc; // exposition only
union {
wrap-sender sndr; // exposition only
};
};
template <scope_token Token, sender Sender>
associate-data(Token, Sender&&) -> associate-data<Token, Sender>;
}
3
For an associate-data
object a
, a.sndr.has_value()
isa.assoc
contextually converts to
true
if and only if an
association was successfully made and is owned by
a
.
associate-data(const associate-data& other)
noexcept(is_nothrow_copy_constructible_v<wrap-sender> &&tokenassoc.try_associate())); noexcept(other.
4
Constraints: copy_constructible<wrap-sender>
is true
.
5
Effects: Value-initializes
Initializes
sndr
and
initializes
token
with
other.token
.
If
other.sndr.has_value()
is false
, no
further effects; otherwise, calls
token.try_associate()
and, if that returns
true
, calls
sndr.emplace(*other.sndr)
and, if that exits with an exception, calls
token.disassociate()
before propagating the exception.assoc
with
other.assoc.try_associate()
.
If assoc
contextually converts to
false
, no further
effects; otherwise, initializes
sndr
with
other.sndr
.
associate-data(associate-data&& other) noexcept(is_nothrow_move_constructible_v<wrap-sender>);
6
Effects: Initializes
Equivalent to sndr
with
std::move(other.sndr)
and initializes
token
with
std::move(other.token)
and then calls
other.sndr.reset()
.associate-data(std::move(other).release())
.
associate-data(pair<assoc-t, sender-ref> parts);
7
Effects: Initializes
assoc
with
std::move(parts.first)
.
If assoc
contextually converts to
false
, no further
effects; otherwise, initializes
sndr
with
std::move(*parts.second)
.
~associate-data();
8
Effects: If
If
sndr.has_value()
returns false
then
no effect; otherwise, invokes
sndr.reset()
before invoking
token.disassociate()
.assoc
contextually converts to
false
then no
effect; otherwise, destroys
sndr
.
optional<pair<Token, wrap-sender>>
pair<assoc-t, scope-ref>
release() && noexcept
(is_nothrow_move_constructible_v<wrap-sender>);
9
Effects: If
sndr.has_value()
returns false
then
returns an optional
that does not contain a value; otherwise returns an
optional
containing
a value of type pair<Token, wrap-sender>
as if by:
return optional(pair(token, std::move(*sndr)));
Constructs an object
u
of type
scope-ref
that is initialized with
nullptr
if
assoc
contextually converts to
false
and with
addressof(sndr)
otherwise, then returns pair{std::move(assoc), std::move(u)}
.
9
Postconditions:
sndr
does
not contain a value.
10 The
name associate
denotes a
pipeable sender adaptor object. For subexpressions
sndr
and
token
, if
decltype((sndr))
does not
satisfy sender
, or remove_cvref_t<decltype((token))>
does not satisfy scope_token
,
then associate(sndr, token)
is
ill-formed.
…
13 The
member impls-for<associate_t>::get-state
is initialized with a callable object equivalent to the following
lambda:
[]<class Sndr, class Rcvr>(Sndr&& sndr, Rcvr& rcvr) noexcept(see below) {&& [_, data] = std::forward<Sndr>(sndr);
auto
auto dataParts = std::move(data).release();
using scope_token = decltype(dataParts->first);
using wrap_sender = decltype(dataParts->second);
using associate_data_t = remove_cvref_t<decltype(data)>;
using assoc_t = typename associate_data_t::assoc-t;
using sender_ref_t = typename associate_data_t::sender-ref;
wrap_sendertypename sender_ref_t::element_type, Rcvr>;
using op_t = connect_result_t<
struct op_state {boolassoc_t associated = false; // exposition only
union {
Rcvr* rcvr; // exposition onlystruct {
scope_token token; // exposition only
op_t op; // exposition only
} assoc; // exposition only
};
explicit op_state(Rcvr& r) noexcept
: rcvr(addressof(r)) {}
explicit op_state(scope_token tkn, wrap_sender&& sndr, Rcvr& r) try
: associated(true),
assoc(tkn, connect(std::move(sndr), std::move(r))) {
}
catch (…) {
tkn.disassociate();
throw;
}
explicit op_state(pair<assoc_t, sender_ref_t> parts, Rcvr& r)
: assoc(std::move(parts.first)) {
if (assoc)
::new (voidify(op)) op_t{
connect(std::move(*parts.second), std::move(r))};
else
rcvr = addressof(r);
}
explicit op_state(associate_data_t&& ad, Rcvr& r)
: op_state(std::move(ad).release(), r) {}
explicit op_state(const associate_data_t& ad, Rcvr& r)
requires copy_constructible<associate_data_t>
: op_state(associate_data_t(ad).release(), r) {}
op_state(op_state&&) = delete;
~op_state() {iated) {
if (assocassoc.op.~op_t();
assoc._token_.disassociate();
assoc._token_.~scope_token();
}
}
void run() noexcept { // exposition onlyiated)
if (assocassoc.op);
start(
else
set_stopped(std::move(*rcvr));
}
};
if (dataParts)
return op_state{std::move(dataParts->first), std::move(dataParts->second), rcvr};
else
return op_state{std::forward_like(data), rcvr};
}
14 The
expression in the noexcept
clause of impls-for<associate_t>::get-state
is
is_nothrow_constructible_v<remove_cvref_t
is_nothrow_move_constructible_v<wrap-sender> &&
(is_rvalue_reference_v<Sndr&&> || is_nothrow_constructible_v<remove_cvref_t
nothrow-callable<connect_t, wrap-sender, Rcvr>
where wrap-sender
is
the type remove_cvref_t<data-type<Sndr>>::wrap-sender.
execution::spawn_future
To the subsection 33.9.12.18 [exec.spawn.future], make the following changes:
7
Let spawn-future-state
be the exposition-only class template:
namespace std::execution {
template<class Alloc, scope_token Token, sender Sender, class Env>
struct spawn-future-state // exposition only
: spawn-future-state-base<completion_signatures_of_t<future-spawned-sender<Sender, Env>>> {
using sigs-t = // exposition only
completion_signatures_of_t<future-spawned-sender<Sender, Env>>;
using receiver-t = // exposition only
spawn-future-receiver<sigs-t>;
using op-t = // exposition only
connect_result_t<future-spawned-sender<Sender, Env>, receiver-t>;
spawn-future-state(Alloc alloc, Sender&& sndr, Token token, Env env) // exposition only
: alloc(std::move(alloc)),
op(connect(
write_env(stop-when(std::forward<Sender>(sndr), ssource.get_token()), std::move(env)),
receiver-t(this))),token(std::move(token)),
iated(token.try_associate()) {
assocassociatedassoc)
if (
start(op);
else
set_stopped(receiver-t(this));
}
void complete() noexcept override; // exposition only
void consume(receiver auto& rcvr) noexcept; // exposition only
void abandon() noexcept; // exposition only
private:
using alloc-t = // exposition only
typename allocator_traits<Alloc>::template rebind_alloc<spawn-future-state>;using assoc-t = // exposition only
remove_cvref_t<decltype(declval<Token&>().try_associate())>;
alloc-t alloc; // exposition only
ssource-t ssource; // exposition only
op-t op; // exposition onlyTokenassoc-t tokenassoc; // exposition only
bool associated; // exposition only
void destroy() noexcept; // exposition only
}; }
…
void destroy() noexcept;
12 Effects: Equivalent to:
auto token = std::move(this->token);
bool associated = this->associated;
auto assoc = std::move(this->assoc);
{
auto alloc = std::move(this->alloc);
allocator_traits<alloc-t>::destroy(alloc, this);
allocator_traits<alloc-t>::deallocate(alloc, this, 1);
}
if (associated)
token.disassociate();
execution::spawn
To the subsection 33.9.13.3 [exec.spawn], make the following changes:
5 Let spawn-state be the exposition-only class template:
namespace std::execution {
template<class Alloc, scope_token Token, sender Sender>
struct spawn-state : spawn-state-base { // exposition only
using op-t = connect_result_t<Sender, spawn-receiver>; // exposition only
spawn-state(Alloc alloc, Sender&& sndr, Token token); // exposition only
void complete() noexcept override; // exposition only noexcept; // exposition only
void run()
private:
using alloc-t = // exposition only
typename allocator_traits<Alloc>::template rebind_alloc<spawn-state>;using assoc-t = // exposition only
remove_cvref_t<decltype(declval<Token&>().try_associate())>;
alloc-t alloc; // exposition only
op-t op; // exposition onlyTokenassoc-t tokenassoc; // exposition only
void destroy() noexcept; // exposition only
}; }
spawn-state(Alloc alloc, Sender&& sndr, Token token);
6
Effects: Initializes
Initializes
alloc
with
alloc
,
token
with
token
, and
op
with:
connect(std::move(sndr), spawn-receiver(this))
alloc
with
std::move(alloc)
,
op
with
connect(std::move(sndr), spawn-receiver(this))
,
and assoc
with
token.try_associate()
.
void run() noexcept;
7 Effects: Equivalent to:
if (token.try_associate())
start(op);
elsedestroycomplete();
void complete() noexcept override;
8 Effects: Equivalent to:
auto token = std::move(this->token);
destroy();
token.disassociate();
void destroy() noexcept;
9
Effects: Equivalent to:
auto assoc = std::move(this->assoc);
auto alloc = std::move(this->alloc);
allocator_traits<alloc-t>::destroy(alloc, this); allocator_traits<alloc-t>::deallocate(alloc, this, 1);
At the beginning of subsection 33.14.1 [exec.scope.concepts], make the following changes
1 The
scope_assocation
concept defines the requirements on a type
Assoc
that, when
engaged, owns an association with an async scope.
namespace std::execution {
template <class Assoc>
concept scope_association =
movable &&
default_initializable &&
requires(const Assoc assoc) {
{ static_cast(assoc) } noexcept;
{ assoc.try_associate() } -> same_as;
};
}
2
The scope_token
concept defines
the requirements on a type Token
that can be used to create associations between senders and an async
scope.
3
Let test-sender
and
test-env
be unspecified
types such that sender_in<test-sender, test-env>
is modeled.
namespace std::execution {
template <class Token>
concept scope_token =
copyable<Token> &&
requires(const Token token) {{ token.try_associate() } -> same_as;
{ token.disassociate() } noexcept -> same_as;
{ token.try_associate() } -> scope_association;
{ token.wrap(declval<test-sender>()) } -> sender_in<test-env>;
}; }
execution::simple_counting_scope
and execution::counting_scope
To the subsection 33.14.2.2.1 [exec.scope.simple.counting.general], make the following change:
namespace std::execution {
class simple_counting_scope {
public:
// [exec.simple.counting.token], token
struct token;
// [exec.simple.counting.assoc], assoc
struct assoc;
static constexpr size_t max_associations = implementation-defined;
// [exec.simple.counting.ctor], constructor and destructor
simple_counting_scope() noexcept;
simple_counting_scope(simple_counting_scope&&) = delete;
~simple_counting_scope();
// [exec.simple.counting.mem], members
token get_token() noexcept;
void close() noexcept;
sender auto join() noexcept;
private:
size_t count; // exposition only
scope-state-type state; // exposition only
boolassoc try-associate() noexcept; // exposition only
void disassociate() noexcept; // exposition only
template<class State>
bool start-join-sender(State& state) noexcept; // exposition only
}; }
To the subsection 33.14.2.2.3 [exec.simple.counting.mem], make the following changes:
boolassoc try-associate() noexcept;
5
Effects: If
count
is equal to
max_associations
, then no
effects. Otherwise, if
state
is
unused
, then increments
count
and changes
state
to
open
;open
or
open-and-joining
, then
increments count
;6
Returns: An
object true
if count
was incremented,
false
otherwise.a
of type
simple_counting_scope::assoc
such that
a.scope
is
this
if
count
was
incremented,
nullptr
otherwise.
To the subsection 33.14.2.2.4 [exec.simple.counting.token], make the following changes:
namespace std::execution {
struct simple_counting_scope::token {
template<sender Sender>
Sender&& wrap(Sender&& snd) const noexcept;boolassoc try_associate() const noexcept;
void disassociate() const noexcept;
private:
simple_counting_scope* scope; // exposition only
}; }
template <sender Sender> Sender&& wrap(Sender&& snd) const noexcept;
1
Returns:
std::forward<Sender>(snd)
.
boolassoc try_associate() const noexcept;
2
Effects: Equivalent to: return scope->try-associate();
void disassociate() const noexcept;
3
Effects: Equivalent to
scope->disassociate()
.
Add the following new section immediately after 33.14.2.2.4 [exec.simple.counting.token]:
Association [exec.simple.counting.assoc]
namespace std::execution {
struct simple_counting_scope::assoc {
explicit operator bool() const noexcept;
assoc try_associate() const noexcept;
private:
using handle = // exposition only
unique_ptr<simple_counting_scope, decltype([](auto* p) noexcept {
p->disassociate();
})>;
handle scope; // exposition only
};
}
explicit operator bool() const noexcept;
1
Returns:
scope != nullptr
assoc try_associate() const noexcept;
2
Returns: A default-initialized
assoc
if
scope
is
nullptr
,
scope->try-associate()
otherwise.
To the subsection 33.14.2.3 [exec.scope.counting], make the following changes:
namespace std::execution {
class counting_scope {
public:struct assoc {
explicit operator bool() const noexcept;
assoc try_associate() const noexcept;
private:
[using _handle_ = // _exposition\ only_]{.add}unique_ptr<counting_scope, decltype([](auto* p) noexcept {
[p->_disassociate_();]{.add}})>;
[_handle_ _scope_; // _exposition\ only_]{.add}};
struct token {
template<sender Sender>
sender auto wrap(Sender&& snd) const noexcept(see below);boolassoc try_associate() const noexcept;
void disassociate() const noexcept;
private:
counting_scope* scope; // exposition only
};
static constexpr size_t max_associations = implementation-defined;
counting_scope() noexcept;
counting_scope(counting_scope&&) = delete;
~counting_scope();
token get_token() noexcept;
void close() noexcept;
sender auto join() noexcept;
void request_stop() noexcept;
private:
size_t count; // exposition only
scope-state-type state; // exposition only
inplace_stop_source s_source; // exposition only
boolassoc try-associate() noexcept; // exposition only
void disassociate() noexcept; // exposition only
template<class State>
bool start-join-sender(State& state) noexcept; // exposition only
}; }