1. Motivation
The generative capability of reflection as they were introduced in C++26 are limited to , with no quick paths to more powerful facilities (See [p3294r2] for example). We can already leverage to impressive effects (See a JSON parser here), here we introduce another basic and lightweight building block: .
enum-based variant
Using we can simulate some enum-based discriminant
template < class ... Ts > struct named_variant_T { std :: variant < Ts ... > v ; enum class kind : std :: size_t ; consteval { std :: vector < std :: meta :: info > enumerators ; for ( auto type : { ^^ Ts ...}) { enumerators . push_back ( enumerator_spec ({ . name = std :: define_static_string ( identifier_of ( type )) })); } enumerators . push_back ( enumerator_spec ({. name = "Default" })); define_enum ( ^^ kind , enumerators ); } constexpr kind which () const noexcept { auto i = v . index (); if ( i == std :: variant_npos ) return kind :: Default ; return static_cast < kind > ( i ); } // ... }; using Message = named_variant_T < X , Y , Z > ;
This unlocks a that has some favorable properties
Looking at a simple example of non- based access
| Before | After |
|---|---|
|
|
variant < Dog , Tanuki , Cat > is later augmented with variant < Dog , Tanuki , Racoon , Cat > , or the variant are shuffled, the index-based switch table will break silently, not the enum based one.
Now, to be fair, visit () does not suffer this limitation. However we should recognize that
-
It is considerably more verbose
-
switch tables lend themselves easily to jump table dispatch (which is fast, which is nice)
See this example where we also leverage annotations on enumerators.
2. What about unscoped enum ?
Note that above, we made the explicit choice of targetting scoped enumeration only, and so the following is not valid
Our rationale behind this conservative approach is entirely rooted in our implementation experience (granted not extensive). Synthesizing enumerators of a scoped enum is a fairly simple operation, on the other hand rewiring the proper context for enumerators of an unscoped enum is quite more troublesome and error prone... Also having constants popping out of existing in a fairly large scope could be troublesome... Hence, for now, we made the cautious choice of limiting the operation to scoped enum only.enum HealthySnacks : int ; consteval { define_enum ( ^^ HealthySnacks , { enumerator_spec ({. name = "Carrot" }), enumerator_spec ({. name = "Celeri" }), enumerator_spec ({. name = "BabaAuRhum" }) }); } // From here on out BabaAuRhum == 2
3. Feature
The actual feature proposed here is , allowing to complete an opaque scoped enumeration alongside the description of its enumerators. In turn, relies on a lightweight description of the enumerators (enumerator options), that are passed to .
All those pieces will be described here, ultimately it should feel familiar to any enthusiast.
3.1. enumerator_options
template < integral I > struct enumerator_options { string name ; optional < I > value = {}; vector < info > annotations = {}; vector < info > attributes = {}; };
As when defining manually an enumerator, the integral is optional. If ommited it will be computed in the same fashion that is done already (incrementing previous value).
Diverging with the original design of , annotations and attributes are directly supported here via and .
Finally note that the support for here relies entirely on the adoption of attributes reflection via [p3385r7].
3.2. enumerator_spec
consteval info enumerator_spec ( enumerator_options props );
enumerator_spec returns the reflection of an enumerator description from the passed in properties.
3.3. is_enumerator_spec
consteval bool is_enumerator_spec ( info r );
is_enumerator_spec returns true when r is a reflection returned by enumerator_spec . It is kept distinct from the reflection of an enumerator, since, while similar on some front they are used for distinct purposes.
3.4. define_enum
Finallytemplate < reflection_range R = initializer_list < info >> consteval info define_enum ( info targetEnum , R && members );
define_enum completes a scoped enumeration declaring a set of enumerators under it. We pass in the reflection of the opaque scoped enumeration we want to complete, and a sequence of reflection obtained via enumerator_spec .
As was the decision for define_aggregate () , when it comes to running compile time operations with side effect, we force define_enum to appear within a consteval block.
Only following the block, is the enumeration completed with its enumerators as specified.
4. Wording
4.1. Library
4.1.1. Meta synopsis [meta.syn]
Add after [meta.reflection.define.aggregate]namespace std {
// ...
// [meta.reflection.define.enum], enum definition generation
template < integral I > struct enumerator_options ;
consteval info enumerator_spec ( enumerator_options options );
consteval bool is_enumerator_spec ( info r );
template < reflection_range R = initializer_list < info >>
consteval info define_enum ( info targetEnum , R && members );
4.1.2. Reflection union definition generation [meta.reflection.define.enum]
namespace std :: meta {
template < integral I >
struct enumerator_options {
optional < string > name = std :: nullopt ;
optional < I > value = {};
vector < info > attributes = {};
vector < info > annotations = {};
};
}
consteval info enumerator_spec ( enumerator_options options );
-
N is either the identifier held by
or ⊥ ifoptions . name does not contain a value,options . name -
V is either the value held by
or ⊥ ifoptions . value does not contain a value,options . value -
AT is a potentially empty sequence of attribute reflections from
, andoptions . attributes -
AN is a potentially empty sequence of values
for each r inconstant_of ( r ) options . annotations
meta :: exception unless the following conditions are met:
-
is false, oroptions . name . has_value () is a valid identifier ([lex.name]) that is not a keyword ([lex.key]), andname . value () -
for each r in options.attributes,
is true, andis_attribute ( options . attributes_of [ r ]) -
for each r in
,options . annotations represents a non-array object type, and evaluation oftype_of ( r ) does not exit via an exception.constant_of ( r )
consteval bool is_enumerator_spec ( info r );
true if r represents the reflection of an enumerator description. Otherwise, false
template < reflection_range R = initializer_list < info >>
consteval info define_enum ( info targetEnum , R && members );
E be the enum represented by targetEnum and ri be the ith reflection value in members .
For every ri in members , let (Ni, Vi, ATi, ANi) be the corresponding enumerator description reflection.
Constant when:
-
is true, andis_enumerator_spec ( ri ) -
is a reflection that represents a scoped enumeration type, andtargetEnum -
is an opaque enumeration from every point in the evaluation context, andtargetEnum -
for every pair (
,i ) wherej <i and Ni is not ⊥ and Nj is not ⊥, then either:j -
Ni is not the same identifier as Nj or
-
Ni is the identifier
(U+005f low line)._
-
Effects: Produces an injected declaration ([expr.const]) that defines and has properties as follows:
-
The target scope of
is the scope to whichD belongs ([basic.scope.scope]).E -
The locus of
follows immediately after the core constant expression currently under evaluation.D -
The injected definition has an enumerator-list with one enumerator-definition for each element of
, in order. For the ith element ofmembers , the corresponding enumerator-definition:members -
is preceded by the attributes denoted by the attribute reflections in ATi, and
-
is preceded by an annotation whose underlying constant ([dcl.attr.annotation]) is r for every reflection r ANi
-
has enumerator-name Ni, and
-
has an enumerator-initializer if and only if Vi ≠ ⊥; if present, it is formed from Vi.
-
-
The values of enumerators for which Vi = ⊥ are determined as specified in [dcl.enum] for enumerators without an explicit enumerator-initializer.
targetEnum .