Question

Enum to String in Modern C++: Standard Options and magic_enum

cppstringenumsc++17c++20

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;
  • constexpr functions and containers that can make lookup efficient;
  • in C++17, std::string_view for 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:

  1. Explicit mapping: portable, predictable, and suitable for public or persisted text.
  2. Compiler-assisted library: less source duplication, but depends on compiler-specific techniques and library support.
  3. 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.

Take Quiz

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:

  1. value is MyEnum::BBB, whose underlying value is the character code for '8'.
  2. names already describes the three allowed enum values. Because it is constexpr, its data can be prepared during compilation.
  3. The first loop iteration compares MyEnum::AAA with MyEnum::BBB; they differ.
  4. The second iteration compares MyEnum::BBB with MyEnum::BBB; they are equal.
  5. The function returns the string view "BBB" immediately.
  6. 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 as 3.
  • Error messages: tell a user that a request failed with PermissionDenied.
  • Debugging dashboards: display a device state such as Connecting or Offline.
  • 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.

Take Quiz

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

ApproachManual names required?Handles sparse/negative valuesStandard C++Typical use
switchYesYesYesSmall enums and clear control flow
constexpr std::arrayYesYesYesShared lookup data and possible reverse conversion
std::map / std::unordered_mapYesYesYesUsually unnecessary for fixed small enums; useful only for dynamic mappings
magic_enumNo in application codeYes for direct name lookup of named valuesNoInternal diagnostics on supported C++17 compilers
Array indexed by integerYesNo, unless values are designed for itYesDense enums starting at zero
Build-time code generationDefined once in a schema/inputYesBuild-tool dependentLarge 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 typeOwnershipBest use
const char*Points to a null-terminated string literalC-style APIs and simple output
std::string_viewNon-owning view; not necessarily null-terminatedModern C++ read-only text handling
std::stringOwns a copy of textWhen the caller must store or modify independently

For fixed string literals in C++17 code, std::string_view is usually a convenient return type.

Take Quiz

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 switch for 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 a switch or table.
  • Return a fallback such as "unknown", or use std::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.

Take Quiz

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.

Take Quiz

Keep learning

Related questions

View more