1. Introduction
This paper is condensed from Arthur O’Dwyer’s blog post "Two kinds of tag types: and "
(2025-12-03). However, the impetus for that blog post and for this proposal itself was identified
by Yi’an Ye in the std-proposals thread "[execution] sender_t or sender_tag?" (2025-11-10).
2. Two kinds of tag types
In C++, when we have a type that carries no data — whose only "identity" is its type —we conventionally call that a "tag type." When you see code like it’s
often the case that is a tag type. However, not all tag types are created equal:
there are at least two major disjoint use-cases for tag types, and the STL (as of 2025)
therefore uses two distinct naming conventions for their identifiers.
There is no widely recognized nomenclature for these two kinds of tag types, as far as I know, so I’m going to call them disambiguation tags and concept tags.
2.1. Disambiguation tag types
A disambiguation tag type looks like this:
struct foo_t {}; // maybe with an explicit ctor inline constexpr foo_t foo = foo_t ();
A disambiguation tag is passed explicitly by the caller to select among overloads of a library function —typically, a constructor. (Because otherwise we could use the function name itself to disambiguate; but all constructors belong to the same overload set by definition.) So we end up with code like this:
struct useful_class { explicit useful_class ( foo_t , int , int ); explicit useful_class ( bar_t , int , int ); }; auto myVar = useful_class ( foo , 1 , 2 );
In this pattern the tag’s name (sans ) denotes a verb or prepositional phrase, or some kind of description
of the functionality you’re asking for — a description that we would have put into the name of the
function, except that we can’t, because the function is a constructor. Examples from the STL include:
All of the above are passed only to constructors.
is passed to .
is passed to .
2.2. Concept tag types
A concept tag type looks like this instead:
struct foo_tag {}; template < class T > concept foo = ~~~~ ;
A concept tag is never named explicitly at the call-site. It is used in generic programming —that is, inside a template that doesn’t know quite all the capabilities of the type it’s received.
So the template will dispatch on a member typedef with a name conventionally ending in
or , which the user-supplied type must provide. Like this:
template < class T > void internal_algorithm ( T , foo_tag ); template < class T > void internal_algorithm ( T , bar_tag ); template < class T > void useful_template ( T t ) { internal_algorithm ( t , typename T :: your_category ()); } struct MyFoolikeClass { using your_category = foo_tag ; };
You can combine this pattern with inheritance, to build a class hierarchy
of concept tags corresponding to your concept hierarchy of concepts. Then my user-supplied
class should (or is assumed to) model your exactly when
is — or is derived from — . The STL does it:
struct forward_iterator_tag : input_iterator_tag {}; template < class I > concept forward_iterator = input_iterator < I > && derived_from < ITER_CONCEPT ( I ), forward_iterator_tag > && ~~~~ ;
In this pattern the tag’s name (sans ) always denotes a concept — a category of behaviors
that the user-supplied type has — and the member typedef name always ends in
or . The C++23 STL has only one family of examples:
-
anditerator_category (iterator_concept ,input_iterator_tag ,output_iterator_tag , etc.)forward_iterator_tag
But Boost has many more:
-
(directed_category ,directed_tag , etc.) in Boost.Graphundirected_tag -
(traversal_category , etc.) in Boost.Graphvertex_list_graph_tag -
(fusion_tag ,deque_tag , etc.) in Boost.Fusionvector_tag -
(stepper_category , etc.) in Boost.Numeric.Odeintstepper_tag -
(orientation_category ,row_major_tag , etc.) in Boost.uBlascolumn_major_tag -
(type_category , etc.) in Boost.uBlastensor_tag -
(storage_category ,dense_tag ,packed_tag , etc.) in Boost.uBlassparse_tag
2.3. Differences between the two kinds
| Disambiguation tags | Concept tags |
|---|---|
Ends with
| Ends with
|
| Without the suffix it’s an inline constexpr variable | Without the suffix it’s a concept |
| Used in concrete programming | Used only in generic programming |
| No associated member typedef | Associated member typedef ending with or
|
| Named in the caller | Named in the typedef definition, and in the callee |
| Callee is usually a constructor | Callee is generic-programming machinery |
| Never derive from each other | May derive from each other to form a hierarchy |
| No associated concept | Concept often involves
|
2.4. Wrong-naming in < execution >
The header (added in P2300)
adds four new "concept tags," following the pattern precisely
to the letter, except that (as of December 2025) it misnames them using rather
than . In each case we have the tag type and its associated concept:
struct sender_t {}; template < class Sndr > concept sender = ~~~~ ;
User code (e.g. this example from [exec.cmplsig]/2)
has a member typedef ending in :
struct my_sender { using sender_concept = sender_t ; ~~~~ };
This follows the "concept tag" pattern to the letter... except for the naming of !
Property of
| Disambiguation tag-like | Concept tag-like |
|---|---|---|
Ends with
| ✅ | ❌ |
| Without the suffix it’s a concept | ❌ | ✅ |
| Used only in generic programming | ❌ | ✅ |
Associated member typedef ending with
| ❌ | ✅ |
| Named in the typedef definition only | ❌ | ✅ |
| Callee is generic-programming machinery | ❌ | ✅ |
| No derived classes in the Standard today | ✅ | ✅ |
Concept involves
| ❌ | ✅ |
So really "should" have been named , as it has been
(organically and independently, as far as we know) in
jaredhoberock/croquet since 2019,
janciesko/stdexx since June 2025,
Cra3z/coio since August 2025,
and maybe a few other places.
In an ideal world, the following four "concept tag" types would be renamed before we finalize C++26:
-
, associated withsender_t andsender_concept concept sender -
, associated withreceiver_t andreceiver_concept concept receiver -
, associated withoperation_state_t andoperation_state_concept concept operation_state -
, associated withscheduler_t andscheduler_concept concept scheduler
3. Additional motivation
While writing up [exec.schedule.from],
we noticed that the type name is shadowed in that section. The local alias
is an alias for , which has a member
aliased to . But this second use of is not
the we’re in the middle of defining; it’s the global concept tag!
This is not formally an ambiguity in the spec, but it’s not very felicitous. Our proposed
change eliminates the notional overloading by cleanly separating the global concept tag
from the local concrete type .
Yi’an predicts that future standards will define not only , but further
derived tags such as , ,
etc., all derived from and indicating concepts that subsume .
That is, we predict that the future evolution of C++ will make conform even more
tightly to the "concept tag" pattern than it does today.
4. Proposed wording
4.1. [execution.syn]
Modify [execution.syn] as follows.
Note that the names of query object types such as and are not changed;
only the names of the four concept tags are changed.
[...] // [exec.sched], schedulers struct scheduler_t ag {}; template < class Sch > concept scheduler = see below ; // [exec.recv], receivers struct receiver_t ag {}; template < class Rcvr > concept receiver = see below ; template < class Rcvr , class Completions > concept receiver_of = see below ; struct set_value_t { unspecified }; struct set_error_t { unspecified }; struct set_stopped_t { unspecified }; inline constexpr set_value_t set_value {}; inline constexpr set_error_t set_error {}; inline constexpr set_stopped_t set_stopped {}; // [exec.opstate], operation states struct operation_state_t ag {}; template < class O > concept operation_state = see below ; struct start_t ; inline constexpr start_t start {}; // [exec.snd], senders struct sender_t ag {}; template < class Sndr > inline constexpr bool enable_sender = see below ; template < class Sndr > concept sender = see below ; [...]
4.2. [exec.sched]
Modify [exec.sched] as follows:
1․ The
concept defines the requirements of a scheduler type ([exec.async.ops]).scheduler is a customization point object that accepts a scheduler. A valid invocation ofschedule is a schedule-expression.schedule [...]namespace std :: execution { template < class Sch > concept scheduler = derived_from < typename remove_cvref_t < Sch >:: scheduler_concept , scheduler_t ag > && queryable < Sch > && requires ( Sch && sch ) { { schedule ( std :: forward < Sch > ( sch )) } -> sender ; { auto ( get_completion_scheduler < set_value_t > ( get_env ( schedule ( std :: forward < Sch > ( sch ))))) } -> same_as < remove_cvref_t < Sch >> ; } && equality_comparable < remove_cvref_t < Sch >> && copyable < remove_cvref_t < Sch >> ; }
4.3. [exec.recv.concepts]
Modify [exec.recv.concepts] as follows:
1․ A receiver represents the continuation of an asynchronous operation. The
concept defines the requirements for a receiver type ([exec.async.ops]). Thereceiver concept defines the requirements for a receiver type that is usable as the first argument of a set of completion operations corresponding to a set of completion signatures. Thereceiver_of customization point object is used to access a receiver’s associated environment.get_env [...]namespace std :: execution { template < class Rcvr > concept receiver = derived_from < typename remove_cvref_t < Rcvr >:: receiver_concept , receiver_t ag > && requires ( const remove_cvref_t < Rcvr >& rcvr ) { { get_env ( rcvr ) } -> queryable ; } && move_constructible < remove_cvref_t < Rcvr >> && // rvalues are movable, and constructible_from < remove_cvref_t < Rcvr > , Rcvr > ; // lvalues are copyable
4.4. [exec.opstate.general]
Modify [exec.opstate.general] as follows:
1․ The
concept defines the requirements of an operation state type ([exec.async.ops]).operation_state [...]namespace std :: execution { template < class O > concept operation_state = derived_from < typename O :: operation_state_concept , operation_state_t > && requires ( O & o ) { start ( o ); }; }
4.5. [exec.snd.expos]
Modify [exec.snd.expos] as follows:
[...] template < class Sndr , class Rcvr , class Index > requires valid - specialization < env - type , Index , Sndr , Rcvr > struct basic - receiver { // exposition only using receiver_concept = receiver_t ag ; [...] template < class Sndr , class Rcvr > requires valid - specialization < state - type , Sndr , Rcvr > && valid - specialization < connect - all - result , Sndr , Rcvr > struct basic - operation : basic - state < Sndr , Rcvr > { // exposition only using operation_state_concept = operation_state_t ag ; [...] template < class Tag , class Data , class ... Child > struct basic - sender : product - type < Tag , Data , Child ... > { // exposition only using sender_concept = sender_t ag ; [...] struct not - a - sender { using sender_concept = sender_t ag ; [...]
4.6. [exec.snd.concepts]
Modify [exec.snd.concepts] as follows:
[...][...]template < class Sndr > concept is - sender = // exposition only derived_from < typename Sndr :: sender_concept , sender_t ag > ;
4.7. [exec.connect]
Modify the example in [exec.connect] as follows:
4. Let
be the following exposition-only class:operation - state - task [...]namespace std :: execution { struct operation - state - task { // exposition only using operation_state_concept = operation_state_t ag ;
4.8. [exec.schedule.from]
Modify [exec.schedule.from] as follows.
Note that the name of the local type in paragraph 6 is not changed; only the
name of the global concept tag in paragraph 10 is changed.
6. The member
is initialized with a callable object equivalent to the following lambda:impls - for < schedule_from_t >:: get - state [...][] < class Sndr , class Rcvr > ( Sndr && sndr , Rcvr & rcvr ) noexcept ( see below ) requires sender_in < child - type < Sndr > , FWD - ENV - T ( env_of_t < Rcvr > ) > { auto & [ _ , sch , child ] = sndr ; using sched_t = decltype ( auto ( sch )); using variant_t = see below ; using receiver_t = see below ; using operation_t = connect_result_t < schedule_result_t < sched_t > , receiver_t > ; constexpr bool nothrow = noexcept ( connect ( schedule ( sch ), receiver_t { nullptr })); 10.
is an alias for the following exposition-only class:receiver_t [...]namespace std :: execution { struct receiver - type { using receiver_concept = receiver_t ag ; state - type * state ; // exposition only
4.9. [exec.let]
Modify [exec.let] as follows:
6. Let
denote the following exposition-only class template:receiver2 [...]namespace std :: execution { template < class Rcvr , class Env > struct receiver2 { using receiver_concept = receiver_t ag ;
4.10. [exec.spawn.future]
Modify [exec.spawn.future] as follows:
5. Let
be the exposition-only class template:spawn - future - receiver [...]namespace std :: execution { template < class Completions > struct spawn - future - receiver { // exposition only using receiver_concept = receiver_t ag ;
4.11. [exec.sync.wait]
Modify [exec.sync.wait] as follows:
[...][...]template < class Sndr > struct sync - wait - receiver { // exposition only using receiver_concept = execution :: receiver_t ag ;
4.12. [exec.spawn]
Modify [exec.spawn] as follows:
[...][...]struct spawn - receiver { // exposition only using receiver_concept = receiver_t ag ;
4.13. [exec.cmplsig]
Modify the example in [exec.cmplsig] as follows:
[Example 1:Declaresstruct my_sender { using sender_concept = sender_t ag ; using completion_signatures = execution :: completion_signatures < set_value_t (), set_value_t ( int , float ), set_error_t ( exception_ptr ), set_error_t ( error_code ), set_stopped_t () > ; }; to be a sender that [...] —end example]my_sender
4.14. [exec.as.awaitable]
Modify [exec.as.awaitable] as follows:
3.
is equivalent to:awaitable - receiver struct awaitable - receiver { using receiver_concept = receiver_t ag ; variant < monostate , result - type , exception_ptr >* result - ptr ; // exposition only coroutine_handle < Promise > continuation ; // exposition only // see below };
4.15. [exec.inline.scheduler]
Modify [exec.inline.scheduler] as follows:
namespace std :: execution { class inline_scheduler { class inline - sender ; // exposition only template < receiver R > class inline - state ; // exposition only public : using scheduler_concept = scheduler_t ag ; constexpr inline - sender schedule () noexcept { return {}; } constexpr bool operator == ( const inline_scheduler & ) const noexcept = default ; }; }
4.16. [exec.task.scheduler]
Modify [exec.task.scheduler] as follows:
[...]class task_scheduler { class ts - sender ; // exposition only template < receiver R > class state ; // exposition only public : using scheduler_concept = scheduler_t ag ; template < class Sch , class Allocator = allocator < void >> requires ( ! same_as < task_scheduler , remove_cvref_t < Sch >> ) && scheduler < Sch > explicit task_scheduler ( Sch && sch , Allocator alloc = {}); ts - sender schedule (); friend bool operator == ( const task_scheduler & lhs , const task_scheduler & rhs ) noexcept ; template < class Sch > requires ( ! same_as < task_scheduler , Sch > ) && scheduler < Sch > friend bool operator == ( const task_scheduler & lhs , const Sch & rhs ) noexcept ; private : shared_ptr < void > sch_ ; // exposition only }; [...]class task_scheduler :: ts - sender { // exposition only public : using sender_concept = sender_t ag ; template < receiver Rcvr > state < Rcvr > connect ( Rcvr && rcvr ); }; template < receiver R > class task_scheduler :: state { // exposition only public : using operation_state_concept = operation_state_t ag ; void start () & noexcept ; };
4.17. [task.class]
Modify [task.class] as follows:
[...]namespace std :: execution { template < class T , class Environment > class task { // [task.state] template < receiver Rcvr > class state ; // exposition only public : using sender_concept = sender_t ag ; using completion_signatures = see below ;
4.18. [task.state]
Modify [task.state] as follows:
[...]namespace std :: execution { template < class T , class Environment > template < receiver Rcvr > class task < T , Environment >:: state { // exposition only public : using operation_state_concept = operation_state_t ag ;
4.19. [exec.counting.scopes.general]
Modify [exec.counting.scopes.general] as follows:
[...][...]template <> struct impls - for < scope - join - t > : default - impls { template < class Scope , class Rcvr > struct state { // exposition only struct rcvr - t { // exposition only using receiver_concept = receiver_t ag ;