Question
How can an enum class value be converted to its enumerator name as a string using modern C++ features such as C++11, C++14, C++17, or C++20?
Consider an enum with non-zero, negative, computed, and char-based values:
#include <iostream>
struct MyClass {
enum class MyEnum : char {
AAA = -8,
BBB = '8',
CCC = AAA + BBB
};
};
const char* magic(MyClass::MyEnum value);
int main() {
std::cout << magic(MyClass::MyEnum::AAA) << '\n';
std::cout << magic(MyClass::MyEnum::BBB) << '\n';
std::cout << magic(MyClass::MyEnum::CCC) << '\n';
}
The desired solution should avoid a runtime std::map, support sparse and negative values, work with scoped enums and arbitrary underlying types, and preferably be constexpr, fast, and free of manually maintained enum-to-string mappings. Is this possible with the C++ standard library, Boost, or a modern third-party solution?
Short Answer
An enum stores an integer-like value, not its source-code name. Therefore, C++11 through C++20 provide no fully standard, automatic enum-to-string conversion or standardized compile-time reflection.
For portable production code, define an explicit mapping, usually as a constexpr table and a small lookup function. If avoiding the mapping is more important than strict ISO C++ portability, a third-party library such as magic_enum can derive names on supported compilers. This page explains the trade-offs and shows both approaches.
Concept
An enumerator name such as MyEnum::AAA exists in C++ source code, but an enum object typically contains only its underlying numeric value. For example:
MyClass::MyEnum value = MyClass::MyEnum::AAA;
value stores the equivalent of -8 in its underlying type; it does not inherently store the text "AAA" or "MyClass::MyEnum::AAA".
This matters because a compiled program normally does not need identifier names to perform its work. Compilers may keep names in debug information, but debug information is not a portable runtime API and is often omitted from release builds.
What standard C++ provides
C++11, C++14, C++17, and C++20 provide:
- scoped enums with
enum class; - fixed underlying types such as
enum class Status : char; constexprfunctions and containers that can make lookup efficient;- in C++17,
std::string_viewfor non-owning string results.
They do not provide a standard way to ask, “what was the source spelling of this enumerator?” C++20 did not standardize general reflection either.
As a result, an enum-to-string feature must choose one of these designs:
- Explicit mapping: portable, predictable, and suitable for public or persisted text.
- Compiler-assisted library: less source duplication, but depends on compiler-specific techniques and library support.
- Generated source code: generate both the enum and its mapping from one input definition during the build.
For logging, user interfaces, JSON, and protocols, an explicit mapping is often preferable because the desired output text is part of the application's contract, not merely a copy of a C++ identifier.
Mental Model
Think of an enum as a hotel key card.
- The numeric enum value is the code stored on the card.
- The enumerator name is the label printed in the hotel's records.
After compilation, the program reliably has the code, but it is not guaranteed to have the printed label. To show a label, you either keep a lookup directory yourself or use a tool that inspects compiler-specific information.
A constexpr lookup table is that directory: it pairs known numeric values with names. It also works when room numbers are negative, sparse, or unusual—because it searches for an exact value rather than assuming values begin at zero and increase by one.
Syntax and Examples
Portable C++17 solution: a constexpr lookup table
A small array is often better than std::map for enums. It has no dynamic allocation, can be initialized at compile time, and is easy to search.
#include <array>
#include <iostream>
#include <string_view>
struct MyClass {
enum class MyEnum : char {
AAA = -8,
BBB = '8',
CCC = AAA + BBB
};
};
using MyEnum = MyClass::MyEnum;
constexpr std::string_view to_string(MyEnum value) noexcept {
using Pair = std::pair<MyEnum, std::string_view>;
constexpr std::array<Pair, 3> names{{
{MyEnum::AAA, "AAA"},
{MyEnum::BBB, "BBB"},
{MyEnum::CCC, "CCC"}
}};
for (const auto& [enum_value, name] : names) {
if (enum_value == value) {
return name;
}
}
return "<unknown MyEnum>";
}
int main() {
std::cout << to_string(MyEnum::AAA) << '\n';
std::cout << to_string(MyEnum::BBB) << '\n';
std::cout << to_string(MyEnum::CCC) << '\n';
}
The output is:
AAA
BBB
CCC
The loop compares enum values directly. It does not use array indexes, so AAA = -8, BBB = '8', and the computed value of CCC are all handled correctly.
std::string_view is a lightweight view of a string literal. It does not allocate memory. Return std::string instead only when the caller needs an owned, mutable string.
Avoiding the mapping with magic_enum
magic_enum is a header-only third-party library that uses compiler-specific function-signature details to obtain enum names. It requires C++17 and supported compiler versions.
#include <iostream>
#include <magic_enum/magic_enum.hpp>
struct MyClass {
enum class MyEnum : char {
AAA = -8,
BBB = '8',
CCC = AAA + BBB
};
};
int main() {
const auto name = magic_enum::enum_name(MyClass::MyEnum::AAA);
std::cout << name << '\n';
}
enum_name returns a std::string_view. For a valid, named enumerator, it typically produces "AAA", not the fully qualified text "MyClass::MyEnum::AAA".
Use this approach only after checking the library's supported compilers and behavior for your project. It is convenient, but it is not a facility guaranteed by the ISO C++ standard.
Step by Step Execution
Consider the portable lookup function:
constexpr std::string_view to_string(MyEnum value) noexcept {
constexpr std::array names{
std::pair{MyEnum::AAA, "AAA"},
std::pair{MyEnum::BBB, "BBB"},
std::pair{MyEnum::CCC, "CCC"}
};
for (const auto& [enum_value, name] : names) {
if (enum_value == value) {
return name;
}
}
return "<unknown MyEnum>";
}
For this call:
const auto text = to_string(MyEnum::BBB);
execution proceeds as follows:
valueisMyEnum::BBB, whose underlying value is the character code for'8'.namesalready describes the three allowed enum values. Because it isconstexpr, its data can be prepared during compilation.- The first loop iteration compares
MyEnum::AAAwithMyEnum::BBB; they differ. - The second iteration compares
MyEnum::BBBwithMyEnum::BBB; they are equal. - The function returns the string view
"BBB"immediately. - No map lookup, heap allocation, or numeric indexing is required.
For a value created from an unrecognized underlying value, no pair matches and the function returns "<unknown MyEnum>":
const auto invalid = static_cast<MyEnum>(99);
std::cout << to_string(invalid) << '\n';
Handling this case is important because C++ permits a cast that creates an enum value not declared by the enum.
Real World Use Cases
Enum-to-string conversion is useful whenever a program must show or record a meaningful state.
- Application logs: write
"timeout"instead of an opaque integer such as3. - Error messages: tell a user that a request failed with
PermissionDenied. - Debugging dashboards: display a device state such as
ConnectingorOffline. - Configuration parsing: convert a textual value such as
"warning"into an enum, and serialize it back for diagnostics. - HTTP APIs and JSON: map internal enum states to stable public strings such as
"pending"or"completed". - Command-line tools: print readable values in
--help, diagnostics, and reports.
For external formats, do not automatically expose C++ spelling unless that spelling is deliberately your public contract. A stable API may need "permission_denied" while the C++ enumerator is PermissionDenied.
Real Codebase Usage
Production code commonly keeps conversion at the boundary where text is needed rather than converting enums throughout the program.
Use a total conversion for logs
A logging helper should safely handle invalid values:
constexpr std::string_view to_string(Status value) noexcept {
switch (value) {
case Status::ready: return "ready";
case Status::busy: return "busy";
case Status::failed:return "failed";
}
return "unknown";
}
The fallback protects logs and diagnostics from values received through casts, corrupted data, or version mismatches.
Use validation when converting text to an enum
Parsing is not the same as printing. Parsing can fail, so return an optional result:
#include <optional>
#include <string_view>
std::optional<Status> status_from_string(std::string_view text) noexcept {
if (text == "ready") return Status::ready;
if (text == "busy") return Status::busy;
if (text == "failed") return Status::failed;
return std::nullopt;
}
Prefer switch for a small enum
For a handful of values, a switch is often the clearest mapping. For a larger enum, a table can centralize pairs and can also support reverse lookup.
Keep public names intentional
If strings are written to files, databases, URLs, or network messages, define those strings explicitly. Renaming a C++ enumerator should not silently change stored or transmitted data.
Common Mistakes
Assuming enum values are consecutive
This approach fails for negative or sparse values:
// Broken: AAA is -8, so this indexes before the array.
const char* names[] = {"AAA", "BBB", "CCC"};
return names[static_cast<int>(value)];
Use a switch or a value-to-name table instead.
Forgetting unknown values
An enum variable can hold an undeclared value after a cast:
const auto value = static_cast<MyEnum>(99);
Always provide a fallback string, or return std::optional<std::string_view> when absence must be handled explicitly.
Using std::map for a tiny fixed mapping
std::map<MyEnum, const char*> names = {
{MyEnum::AAA, "AAA"}
};
This works, but a local non-static map is rebuilt on every call. Even a static map is usually unnecessary for a small fixed enum. A switch or constexpr std::array is simpler and avoids tree lookup.
Expecting static_cast<int> to reveal the name
std::cout << static_cast<int>(MyEnum::AAA); // prints -8, not "AAA"
A cast exposes the underlying numeric representation only.
Treating compiler tricks as standard reflection
Libraries such as magic_enum are useful, but their implementation relies on compiler behavior. Check compiler support and test upgrades. Use an explicit mapping when exact portability or stable serialized text is required.
Ignoring aliases
Two enumerators may have the same value:
enum class Result { success = 0, ok = 0 };
A value-to-string conversion cannot uniquely recover whether the source wrote success or ok; at runtime they are the same value. Choose one canonical output name.
Comparisons
| Approach | Manual names required? | Handles sparse/negative values | Standard C++ | Typical use |
|---|---|---|---|---|
switch | Yes | Yes | Yes | Small enums and clear control flow |
constexpr std::array | Yes | Yes | Yes | Shared lookup data and possible reverse conversion |
std::map / std::unordered_map | Yes | Yes | Yes | Usually unnecessary for fixed small enums; useful only for dynamic mappings |
magic_enum | No in application code | Yes for direct name lookup of named values | No | Internal diagnostics on supported C++17 compilers |
| Array indexed by integer | Yes | No, unless values are designed for it | Yes | Dense enums starting at zero |
| Build-time code generation | Defined once in a schema/input | Yes | Build-tool dependent | Large APIs, protocol definitions, and generated models |
switch versus lookup table
A switch directly expresses one result per case and is ideal for a small enum. A table is useful when you need the same mapping for several operations, such as printing, parsing, and presenting choices. Neither has to depend on enumerator values being contiguous.
const char* versus std::string_view
| Return type | Ownership | Best use |
|---|---|---|
const char* | Points to a null-terminated string literal | C-style APIs and simple output |
std::string_view | Non-owning view; not necessarily null-terminated | Modern C++ read-only text handling |
std::string | Owns a copy of text | When the caller must store or modify independently |
For fixed string literals in C++17 code, std::string_view is usually a convenient return type.
Cheat Sheet
- An enum value does not standardly retain its enumerator name.
- C++11 through C++20 have no standardized general enum reflection or enum-to-string function.
- Use
switchfor a small, explicit mapping. - Use
constexpr std::array<std::pair<Enum, std::string_view>, N>for a portable table. - Do not index a name array with the underlying value unless the enum is intentionally dense and zero-based.
- Sparse, negative, computed, and
char-based enum values work with equality comparisons in aswitchor table. - Return a fallback such as
"unknown", or usestd::optional<std::string_view>, for invalid values. - Enum aliases cannot be uniquely converted back to their original spelling because equal enum values are indistinguishable.
magic_enum::enum_name(value)is a convenient C++17 third-party option on supported compilers, not an ISO C++ guarantee.- For JSON, files, and APIs, explicitly choose stable external strings rather than coupling data formats to C++ names.
FAQ
Is there a standard std::to_string for enums in C++?
No. std::to_string accepts numeric types, not arbitrary enum types, and it would produce a number rather than an enumerator name even if you cast the enum first.
Can C++20 automatically get an enum name?
No. C++20 did not standardize general reflection that exposes enumerator names. You still need a mapping, source generation, or a non-standard library technique.
Does magic_enum work with enum class and custom underlying types?
It is designed to support scoped enums and common underlying types on its supported C++17 compilers. Verify the library documentation and test your exact compiler and version.
Does magic_enum return MyClass::MyEnum::AAA?
Typically, magic_enum::enum_name(MyEnum::AAA) returns the unqualified enumerator name, "AAA". If you require a fully qualified or user-facing label, define that text explicitly.
Why not use std::map for enum-to-string conversion?
It is valid but usually more machinery than needed for a fixed set of enumerators. A switch or constexpr array is simpler, avoids dynamic allocation, and is typically faster for small sets.
Can I convert every integer to an enum name?
No. Only values corresponding to declared enumerators have names. A cast can create other enum values, so conversion needs an unknown-value policy.
What should I use for strings stored in JSON or a database?
Use an explicit mapping with stable external names. This prevents a source-code rename from accidentally changing persisted data or an API contract.
Can duplicate enum values have different returned names?
Not reliably from the value alone. If two enumerators have the same underlying value, runtime code cannot know which alias was originally written. Choose one canonical name.
Mini Project
Description
Build a status formatter for a command-line or service application. The enum deliberately uses non-consecutive values so the solution cannot rely on numeric array indexing. The formatter will produce readable text for logs and safely report unexpected values.
Goal
Create a portable C++17 function that converts each known JobStatus value to text and handles an invalid value safely.
Requirements
Define JobStatus as a scoped enum with at least one negative and one sparse numeric value.
Implement to_string(JobStatus) returning std::string_view.
Use a constexpr value-to-name table rather than indexing by the underlying integer.
Return a clear fallback for an unknown enum value.
Print all known statuses and one invalid status from main.
Keep learning
Related questions
Advantages of Brace Initialization in C++
Learn why C++ brace initialization is often clearer and safer than other object initialization styles, with examples and common pitfalls.
Basic Rules and Idioms for Operator Overloading in C++
Learn the core rules, syntax, and common idioms for operator overloading in C++, including member vs non-member operators.
C++ Aggregates, Trivial Types, Trivially Copyable Types, and PODs Explained
Learn what aggregates, trivial types, trivially copyable types, and PODs mean in C++, how they differ, and why they matter.