short

When two enumerators share a value, the value carries both names

· english· audience: working-cpp· AI-generated, reviewed by Filip Sajdak

Bogusław Branicki left a comment on LinkedIn under a reflection-based enum_to_string that is worth a full answer. His example:

enum Color { red = 1, green = 1, blue = 2, some_long_name_enum };

enum_to_string(Color::green) returns "red". He also pointed out that once the value sits in a variable, C++26 reflection cannot get the original enumerator back; only ^^ applied to the enumerator itself keeps it. And he suggested that checking for duplicates up front might be worth doing.

He is right on all three. Color::green and Color::red are the same value: a Color equal to 1. There is nothing left in it that says which spelling the programmer typed, so a function taking Color receives both names or neither. The to_string from the enum-to-string post walks enumerators_of and returns the first match, which is red.

What reflection does offer is a way to be explicit about it. The demo below extends that renum library in four directions.

Four answers to one value

All the names. renum::names_of(v) returns every enumerator equal to v as a std::span<char const* const>. The table behind it is built at compile time with define_static_array, holding pointers from define_static_string. A string_view would be the natural element, but it is not a structural type, so it cannot live in a static array produced that way. For green the answer is red|green.

Refuse the ambiguity. renum::unique_values<E>() is consteval and throws a std::meta::exception naming both enumerators when two share a value. Put it in a static_assert on any enum that must not alias, and GCC 16.2 reports:

error: non-constant condition for static assertion
error: uncaught exception of type 'std::meta::exception'; 'what()': 'Color::green duplicates Color::red (= 1)'

Mark the alias. Some aliases are intentional, typically a renamed enumerator whose old spelling stays for source compatibility. P3394R4 lists enumerators among the declarations an annotation may apply to, so the old name can say what it is:

enum class Level {
    debug,
    info,
    warn [[=renum::alias{}]],
    warning = warn,
    error,
};

The alias-aware to_string skips enumerators that carry the tag (found with annotations_of_with_type) and returns warning; first match would return warn.

Keep the reflection. renum::name_of<^^green>() returns green, because ^^green reflects the enumerator, a declaration with its own identity. That identity survives only as long as you pass the reflection around. Converting it to a Color throws it away, which is exactly Bogusław’s point.

The names cost no allocation

His second remark was about the return type. An enum_to_string that returns std::string builds a new string on every call, and a name longer than the small-string buffer allocates. std::meta::identifier_of returns a std::string_view, and [meta.syn] in the C++26 draft says what it points at: the characters are null-terminated, and each one is an object with static storage duration that is usable in constant expressions. That is why a static constexpr std::string_view can hold the result, and why the demo can static_assert on the terminator.

The demo counts heap allocations with a replaced operator new. some_long_name_enum is 19 characters, past libstdc++’s 15-character inline buffer. Viewing the name allocates nothing; copying it into a std::string allocates once.

names_of(green) = red|green
first match(green) = red
name_of<^^green> = green
names_of(Level::warning) = warn|warning
first match(Level::warning) = warn
to_string(Level::warning) = warning (alias-aware)
some_long_name_enum (19 chars): string_view 0 allocations, std::string 1

What existing libraries return for an alias

library name returned for an aliased value
magic_enum the first declared; its docs give enum_name(ShapeKind::Box) returning "ConvexBegin"
Boost.Describe enum_to_string the last declared: the loop assigns on every match and has no break
Better Enums _to_string documented as undefined which of the names it picks
P2996R13 §3.6 example the first declared, through an early return

None of them can do better than pick one, since they all start from the value. The choice between first, last and unspecified is a convention, and a static_assert(renum::unique_values<E>()) is the way to find out at compile time that you depend on it.


Sources: Bogusław Branicki’s comment · P2996R13 §3.6 · P3394R4 · magic_enum limitations · Boost.Describe enum_to_string.hpp · Better Enums API reference. The demo and the diagnostic were measured with GCC 16.2 on Compiler Explorer and locally on 30 September 2026.