P4033R1
Synthesizing enum at compile time with define_enum

Published Proposal,

This version:
https://wg21.link/P4033R1
Author:
Aurelien Cassagnes
Audience:
SG7, EWG
Project:
ISO/IEC 14882 Programming Languages — C++, ISO/IEC JTC1/SC22/WG21
Target:
C++29

1. Motivation

The generative capability of reflection as they were introduced in C++26 are limited to define_aggregate, with no quick paths to more powerful facilities (See [p3294r2] for example). We can already leverage define_aggregate to impressive effects (See a JSON parser here), here we introduce another basic and lightweight building block: define_enum.


enum-based variant
A common issue with std::variant is index-based access: if variant<Dog, Tanuki, Cat> is later augmented with variant<Dog, Tanuki, Racoon, Cat>, or the alternatives are shuffled, an index-based switch table will break silently. To be fair, visit() does not suffer this limitation. However we should recognize that

  1. It is considerably more verbose

  2. switch tables lend themselves easily to jump table dispatch (which is fast, which is nice)

Using define_enum() we can simulate some enum-based discriminant variant

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 variant that has some favorable properties
Looking at a simple example of non-visit() based access

Before After
using VarPet = std::variant<Dog, Tanuki, Cat>;
// To keep in sync
enum VariantPetIndex {
  Dog,
  Tanuki,
  Cat,
};
//...
VarPet animal(Dog{});
// ...
switch (animal.index()) {
  default: break;
  case VariantPetIndex::Dogs: break;
  case VariantPetIndex::Tanuki: break;
  case VariantPetIndex::Cats: break;
}
using VarPet = named_variant_T<Dog, Tanuki, Cat>;
//...
VarPet animal(Dog{});

switch (animal.which()) {
  case VarPet::kind::Default: break;
  case VarPet::kind::Dog: break;
  case VarPet::kind::Tanuki: break;
  case VarPet::kind::Cats break;
}

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 BeginString 8 string and MsgSeqNum 34 numeric, 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 define_enum() and #embed 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:

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
};
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.

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 data_member_spec appropriately

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);
}
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.
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 define_enum().

template <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);
    }
};
With this wrapper, the usage becomes
enum 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");
A nicer version of it would need to emulate the natural jump in enumerator value, but essentially using 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.

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"})});
}
That, 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 define_enum(), allowing to complete an opaque scoped enumeration alongside the description of its enumerators. In turn, define_enum relies on a lightweight description of the enumerators (enumerator options), that are passed to enumerator_spec().
All those pieces will be described here, ultimately it should feel familiar to any define_aggregate() 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 value 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 0).
Diverging with the original design of define_aggregate(), annotations and attributes are directly supported here via .annotations and .attributes. Finally note that the support for .attributes 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 data_member_spec precedent and represents declaration descriptions as meta::info values. This preserves uniformity with define_aggregate, allows is_enumerator_spec-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

template <reflection_range R = initializer_list<info>>
    consteval info define_enum(info targetEnum, R&& members);
Finally 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);
Returns: A reflection of an enumerator description (N, V, AT, AN) where
  • N is the identifier held by options.name,

  • V is either the value held by options.value or ⊥ if options.value does not contain a value,

  • AT is a potentially empty sequence of attribute reflections from options.attributes, and

  • AN is a potentially empty sequence of values constant_of(r) for each r in options.annotations

Throws: meta::exception unless the following conditions are met:
  • name.value() is a valid identifier ([lex.name]) that is not a keyword ([lex.key]), and

  • for each r in options.attributes, is_attribute(options.attributes_of[r]) is true, and

  • for each r in options.annotations, type_of(r) represents a non-array object type, and evaluation of constant_of(r) does not exit via an exception.

consteval bool is_enumerator_spec(info r);
Returns: 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);
Let 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_enumerator_spec(ri) is true, and

  • targetEnum is a reflection that represents a scoped enumeration type, and

  • targetEnum is an opaque enumeration from every point in the evaluation context, and

  • for every pair (i, j) where i < j and Ni is not ⊥ and Nj is not ⊥, then either:

    • Ni is not the same identifier as Nj or

    • Ni is the identifier _ (U+005f low line).

Effects: Produces an injected declaration D ([expr.const]) that defines E and has properties as follows:

  • The target scope of D is the scope to which E belongs ([basic.scope.scope]).

  • The locus of D follows immediately after the core constant expression currently under evaluation.

  • The injected definition has an enumerator-list with one enumerator-definition for each element of members, in order. For the ith element of members, the corresponding enumerator-definition:

    • 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.

Returns: targetEnum.

5. Status

This proposal was implemented ([clang-implementation]) on top of the Clang P2996 branch, and is available for test on the same Compiler Explorer branch.

6. Acknowledgements

The original motivation for this facility was discussed by A. Jiang over Github issues. The early version of this proposal greatly benefitted from Matthias Wippich feedback and insights.

References

Normative References

[CLANG-IMPLEMENTATION]
Aurelien Cassagnes. define_enum. URL: https://github.com/bloomberg/clang-p2996/pull/263
[P3294R2]
Barry Revzin, Andrei Alexandrescu, Daveed Vandevoorde. Code Injection with Token Sequences. 15 October 2024. URL: https://wg21.link/p3294r2
[P3385R7]
Aurelien Cassagnes. Attributes reflection. 16 February 2026. URL: https://wg21.link/p3385r7