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
A common issue with is index-based access: if is later augmented with , or the alternatives are shuffled, an index-based switch table will break silently. To be fair, 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)
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 ); } // ... };
This unlocks a that has some favorable properties
Looking at a simple example of non- based access
| Before | After |
|---|---|
|
|
The enum-based approach does not suffer from index drift and stays in sync by construction.
See this example where we also leverage annotations on enumerators.
Protocol dictionaries
Protocol dictionaries often define a set of named integer tags together with per-tag semantic information. For example, a FIX dictionary may contain entries such as and , describing what the tag label/value/representation is.
A version upgrade of FIX usually adds a couple of tags and conserves the previous enumerations, leading to various shenanigans to generate those massive tables under different versions of the standard.
With and we can _embed_ that dictionary, parse it during constant evaluation and synthesize the corresponding C++ representation. Since a version upgrade is additive, we can parse both dictionaries and merge them:
Each synthesized enumerator has the protocol-mandated numeric value and carries annotations describing its deserialization shape and version provenance, while keeping the external protocol dictionary as the single source of truth.struct FixDictionary { enum class TagsV5 : int ; enum class TagsV5Sp2 : int ; consteval { auto v5 = parse_fix_dictionary ( dictionaryv5 , Version :: V5 ); auto sp2Only = parse_fix_dictionary ( dictionaryv5sp2 , Version :: V5SP2 ); define_enum ( ^^ TagsV5 , v5 ); auto v5sp2 = v5 ; v5sp2 . insert ( v5sp2 . end (), sp2Only . begin (), sp2Only . end ()); define_enum ( ^^ TagsV5Sp2 , v5sp2 ); } // From there we have e.g. // enum class TagsV5 : int { // BeginString [[=ShapeTag<String>]] [[=VersionTag<V5>]] = 8, // MsgSeqNum [[=ShapeTag<Numeric>]] [[=VersionTag<V5>]] = 34, // ... // } // and TagsV5Sp2 which is a strict superset };
From there we can look up a field name in the generated enum, read its shape annotation to pick the right C++ type, and emit appropriately
With this the entire protocol, ie. their tags, types, version provenance, and the message layouts, can be driven from external resource files, resolved at compile time, and fully typed at the end of it.struct Heartbeat ; struct Logon ; consteval { // parse message definitions: "Heartbeat 0 TestReqID" // for each field, find the enumerator in TagsV5Sp2, // read its ShapeTag annotation to decide the C++ type // (int for Numeric, string_view for String), // and emit a data_member_spec. auto [ target , members ] = parseMsgLine ( line ); define_aggregate ( target , members ); }
See the full example.
Enum inheritance
One of the nice feature missing from C++ is something like
enum class Levels { Debug , Info , Warning , }; enum class ExtendedLevels : Levels { // Debug, Info, Warning pops here based on : Levels Critical , SuperCritical , };
But this syntax is not available, and therefore the feature is also not happening under this form. What we can do however is, simulate that feature using some wrapper over .
With this wrapper, the usage becomestemplate < typename BaseEnum , std :: meta :: info ... Extra > struct DeriveEnumFrom { enum class result ; consteval { std :: vector < std :: meta :: info > specs ; for ( auto e : std :: meta :: enumerators_of ( ^^ BaseEnum )) { specs . push_back ( std :: meta :: enumerator_spec ({ . name = std :: define_static_string ( std :: string ( std :: meta :: identifier_of ( e )). c_str ()), . value = std :: meta :: reflect_constant ( static_cast < int > ( std :: meta :: extract < BaseEnum > ( e ))), })); } ( specs . push_back ( Extra ), ...); std :: meta :: define_enum ( ^^ result , specs ); } };
A nicer version of it would need to emulate the natural jump in enumerator value, but essentially usingenum class Base { Debug , Info , Warning }; using Derived = DeriveEnumFrom < Base , std :: meta :: enumerator_spec ({. name = std :: define_static_string ( "Critical" )}), std :: meta :: enumerator_spec ({. name = std :: define_static_string ( "SuperCritical" )}) >:: result ; // Later on static_assert ( to_string ( Derived :: Debug ) == "Debug" );
define_enum () some comparable feature is implementable.
Example
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
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
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 sensitive.
That,enum Leaky : int ; void foo () { Leaky l {}; } // ... // So far so good //... consteval { // Error: redefinition of 'foo' as different kind of symbol define_enum ( ^^ Leaky , { enumerator_spec ({. name = "foo" })}); }
foo retrospectively becomes the error trigger when we complete the enum would likely be hostile to users... Hence, for now, we made the cautious choice of limiting the operation to scoped enum only.
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
struct enumerator_options { string name ; info value = {}; vector < info > annotations = {}; vector < info > attributes = {}; };
As when defining manually an enumerator, the integral is optional. If a null reflection is passed, the value will be computed in the same fashion that is done already (incrementing previous value or defaulting to ).
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.
Although an enumerator description could be represented as an ordinary structural type, this paper follows the precedent and represents declaration descriptions as values. This preserves uniformity with , allows -style validation, and leaves room for declaration metadata such as attributes and annotations to be copied from or transformed through reflection.
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
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 {
struct enumerator_options {
string name ;
info value = {};
vector < info > attributes = {};
vector < info > annotations = {};
};
}
consteval info enumerator_spec ( enumerator_options options );
-
N is the identifier held by
,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 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 .