---
title: "Synthesizing enum at compile time with define_enum"
document: P4033R1
date: 2026-06-08
audience: SG7, EWG
reply-to:
  - "Aurelien Cassagnes"
---

## 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](https://brevzin.github.io/c++/2025/06/26/json-reflection/)), 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

<!-- tomd:lossy-table -->

```cpp
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;
}
```

```cpp
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](https://godbolt.org/z/5a3Yenz8d) 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

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

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

### 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].

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

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

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

## Wording

### Library

#### 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);
```

#### 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

r<sub>i</sub>

be the

i<sup>th</sup>

reflection value in

members

.
    For every

r<sub>i</sub>

in

members

, let (

N<sub>i</sub>

,

V<sub>i</sub>

,

AT<sub>i</sub>

,

AN<sub>i</sub>

) 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 *N<sub>i</sub>* is not ⊥ and *N<sub>j</sub>* is not ⊥, then either:
  - *N<sub>i</sub>* is not the same identifier as *N<sub>j</sub>* or
  - *N<sub>i</sub>* 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 *i<sup>th</sup>* element of `members`, the corresponding *enumerator-definition*:
  - is preceded by the attributes denoted by the attribute reflections in *AT<sub>i</sub>*, and
  - is preceded by an annotation whose underlying constant ([dcl.attr.annotation]) is r for every reflection r *AN<sub>i</sub>*
  - has *enumerator-name* *N<sub>i</sub>*, and
  - has an *enumerator-initializer* if and only if *V<sub>i</sub>* ≠ ⊥; if present, it is formed from *V<sub>i</sub>*.
- The values of enumerators for which *V<sub>i</sub>* = ⊥ are determined as specified in [dcl.enum] for enumerators without an explicit *enumerator-initializer*.

Returns:

targetEnum

.

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

## 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](https://github.com/bloomberg/clang-p2996/pull/263). URL: [https://github.com/bloomberg/clang-p2996/pull/263](https://github.com/bloomberg/clang-p2996/pull/263)
[P3294R2]
   Barry Revzin, Andrei Alexandrescu, Daveed Vandevoorde. [Code Injection with Token Sequences](https://wg21.link/p3294r2). 15 October 2024. URL: [https://wg21.link/p3294r2](https://wg21.link/p3294r2)
[P3385R7]
   Aurelien Cassagnes. [Attributes reflection](https://wg21.link/p3385r7). 16 February 2026. URL: [https://wg21.link/p3385r7](https://wg21.link/p3385r7)**
